viberag 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.
Files changed (151) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +219 -0
  3. package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
  4. package/dist/cli/__tests__/mcp-setup.test.js +597 -0
  5. package/dist/cli/app.d.ts +2 -0
  6. package/dist/cli/app.js +238 -0
  7. package/dist/cli/commands/handlers.d.ts +57 -0
  8. package/dist/cli/commands/handlers.js +231 -0
  9. package/dist/cli/commands/index.d.ts +2 -0
  10. package/dist/cli/commands/index.js +2 -0
  11. package/dist/cli/commands/mcp-setup.d.ts +107 -0
  12. package/dist/cli/commands/mcp-setup.js +509 -0
  13. package/dist/cli/commands/useRagCommands.d.ts +23 -0
  14. package/dist/cli/commands/useRagCommands.js +180 -0
  15. package/dist/cli/components/CleanWizard.d.ts +17 -0
  16. package/dist/cli/components/CleanWizard.js +169 -0
  17. package/dist/cli/components/InitWizard.d.ts +20 -0
  18. package/dist/cli/components/InitWizard.js +370 -0
  19. package/dist/cli/components/McpSetupWizard.d.ts +37 -0
  20. package/dist/cli/components/McpSetupWizard.js +387 -0
  21. package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
  22. package/dist/cli/components/SearchResultsDisplay.js +130 -0
  23. package/dist/cli/components/WelcomeBanner.d.ts +10 -0
  24. package/dist/cli/components/WelcomeBanner.js +26 -0
  25. package/dist/cli/components/index.d.ts +1 -0
  26. package/dist/cli/components/index.js +1 -0
  27. package/dist/cli/data/mcp-editors.d.ts +80 -0
  28. package/dist/cli/data/mcp-editors.js +270 -0
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.js +26 -0
  31. package/dist/cli-bundle.cjs +5269 -0
  32. package/dist/common/commands/terminalSetup.d.ts +2 -0
  33. package/dist/common/commands/terminalSetup.js +144 -0
  34. package/dist/common/components/CommandSuggestions.d.ts +9 -0
  35. package/dist/common/components/CommandSuggestions.js +20 -0
  36. package/dist/common/components/StaticWithResize.d.ts +23 -0
  37. package/dist/common/components/StaticWithResize.js +62 -0
  38. package/dist/common/components/StatusBar.d.ts +8 -0
  39. package/dist/common/components/StatusBar.js +64 -0
  40. package/dist/common/components/TextInput.d.ts +12 -0
  41. package/dist/common/components/TextInput.js +239 -0
  42. package/dist/common/components/index.d.ts +3 -0
  43. package/dist/common/components/index.js +3 -0
  44. package/dist/common/hooks/index.d.ts +4 -0
  45. package/dist/common/hooks/index.js +4 -0
  46. package/dist/common/hooks/useCommandHistory.d.ts +7 -0
  47. package/dist/common/hooks/useCommandHistory.js +51 -0
  48. package/dist/common/hooks/useCtrlC.d.ts +9 -0
  49. package/dist/common/hooks/useCtrlC.js +40 -0
  50. package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
  51. package/dist/common/hooks/useKittyKeyboard.js +26 -0
  52. package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
  53. package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
  54. package/dist/common/hooks/useTerminalResize.d.ts +28 -0
  55. package/dist/common/hooks/useTerminalResize.js +51 -0
  56. package/dist/common/hooks/useTextBuffer.d.ts +13 -0
  57. package/dist/common/hooks/useTextBuffer.js +165 -0
  58. package/dist/common/index.d.ts +13 -0
  59. package/dist/common/index.js +17 -0
  60. package/dist/common/types.d.ts +162 -0
  61. package/dist/common/types.js +1 -0
  62. package/dist/mcp/index.d.ts +12 -0
  63. package/dist/mcp/index.js +66 -0
  64. package/dist/mcp/server.d.ts +25 -0
  65. package/dist/mcp/server.js +837 -0
  66. package/dist/mcp/watcher.d.ts +86 -0
  67. package/dist/mcp/watcher.js +334 -0
  68. package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
  69. package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
  70. package/dist/rag/__tests__/helpers.d.ts +30 -0
  71. package/dist/rag/__tests__/helpers.js +67 -0
  72. package/dist/rag/__tests__/merkle.test.d.ts +5 -0
  73. package/dist/rag/__tests__/merkle.test.js +161 -0
  74. package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
  75. package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
  76. package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
  77. package/dist/rag/__tests__/multi-language.test.js +535 -0
  78. package/dist/rag/__tests__/rag.test.d.ts +10 -0
  79. package/dist/rag/__tests__/rag.test.js +311 -0
  80. package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
  81. package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
  82. package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
  83. package/dist/rag/__tests__/search-filters.test.js +250 -0
  84. package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
  85. package/dist/rag/__tests__/search-modes.test.js +133 -0
  86. package/dist/rag/config/index.d.ts +61 -0
  87. package/dist/rag/config/index.js +111 -0
  88. package/dist/rag/constants.d.ts +41 -0
  89. package/dist/rag/constants.js +57 -0
  90. package/dist/rag/embeddings/fastembed.d.ts +62 -0
  91. package/dist/rag/embeddings/fastembed.js +124 -0
  92. package/dist/rag/embeddings/gemini.d.ts +26 -0
  93. package/dist/rag/embeddings/gemini.js +116 -0
  94. package/dist/rag/embeddings/index.d.ts +10 -0
  95. package/dist/rag/embeddings/index.js +9 -0
  96. package/dist/rag/embeddings/local-4b.d.ts +28 -0
  97. package/dist/rag/embeddings/local-4b.js +51 -0
  98. package/dist/rag/embeddings/local.d.ts +29 -0
  99. package/dist/rag/embeddings/local.js +119 -0
  100. package/dist/rag/embeddings/mistral.d.ts +22 -0
  101. package/dist/rag/embeddings/mistral.js +85 -0
  102. package/dist/rag/embeddings/openai.d.ts +22 -0
  103. package/dist/rag/embeddings/openai.js +85 -0
  104. package/dist/rag/embeddings/types.d.ts +37 -0
  105. package/dist/rag/embeddings/types.js +1 -0
  106. package/dist/rag/gitignore/index.d.ts +57 -0
  107. package/dist/rag/gitignore/index.js +178 -0
  108. package/dist/rag/index.d.ts +15 -0
  109. package/dist/rag/index.js +25 -0
  110. package/dist/rag/indexer/chunker.d.ts +129 -0
  111. package/dist/rag/indexer/chunker.js +1352 -0
  112. package/dist/rag/indexer/index.d.ts +6 -0
  113. package/dist/rag/indexer/index.js +6 -0
  114. package/dist/rag/indexer/indexer.d.ts +73 -0
  115. package/dist/rag/indexer/indexer.js +356 -0
  116. package/dist/rag/indexer/types.d.ts +68 -0
  117. package/dist/rag/indexer/types.js +47 -0
  118. package/dist/rag/logger/index.d.ts +20 -0
  119. package/dist/rag/logger/index.js +75 -0
  120. package/dist/rag/manifest/index.d.ts +50 -0
  121. package/dist/rag/manifest/index.js +97 -0
  122. package/dist/rag/merkle/diff.d.ts +26 -0
  123. package/dist/rag/merkle/diff.js +95 -0
  124. package/dist/rag/merkle/hash.d.ts +34 -0
  125. package/dist/rag/merkle/hash.js +165 -0
  126. package/dist/rag/merkle/index.d.ts +68 -0
  127. package/dist/rag/merkle/index.js +298 -0
  128. package/dist/rag/merkle/node.d.ts +51 -0
  129. package/dist/rag/merkle/node.js +69 -0
  130. package/dist/rag/search/filters.d.ts +21 -0
  131. package/dist/rag/search/filters.js +100 -0
  132. package/dist/rag/search/fts.d.ts +32 -0
  133. package/dist/rag/search/fts.js +61 -0
  134. package/dist/rag/search/hybrid.d.ts +17 -0
  135. package/dist/rag/search/hybrid.js +58 -0
  136. package/dist/rag/search/index.d.ts +89 -0
  137. package/dist/rag/search/index.js +367 -0
  138. package/dist/rag/search/types.d.ts +130 -0
  139. package/dist/rag/search/types.js +4 -0
  140. package/dist/rag/search/vector.d.ts +25 -0
  141. package/dist/rag/search/vector.js +44 -0
  142. package/dist/rag/storage/index.d.ts +92 -0
  143. package/dist/rag/storage/index.js +287 -0
  144. package/dist/rag/storage/lancedb-native.d.ts +7 -0
  145. package/dist/rag/storage/lancedb-native.js +10 -0
  146. package/dist/rag/storage/schema.d.ts +23 -0
  147. package/dist/rag/storage/schema.js +50 -0
  148. package/dist/rag/storage/types.d.ts +100 -0
  149. package/dist/rag/storage/types.js +68 -0
  150. package/package.json +67 -0
  151. package/scripts/check-node-version.js +37 -0
@@ -0,0 +1,597 @@
1
+ /**
2
+ * MCP Setup Integration Tests
3
+ *
4
+ * Tests for config generation, file creation, merging, and edge cases.
5
+ */
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { writeMcpConfig, generateMcpConfig, generateViberagConfig, generateOpenCodeViberagConfig, generateTomlConfig, readJsonConfig, mergeConfig, hasViberagConfig, isAlreadyConfigured, configExists, removeViberagConfig, removeViberagFromConfig, findConfiguredEditors, } from '../commands/mcp-setup.js';
11
+ import { EDITORS, getEditor } from '../data/mcp-editors.js';
12
+ /**
13
+ * Create a temporary directory for test isolation.
14
+ */
15
+ async function createTempDir() {
16
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'viberag-mcp-test-'));
17
+ return {
18
+ dir,
19
+ cleanup: async () => {
20
+ await fs.rm(dir, { recursive: true, force: true });
21
+ },
22
+ };
23
+ }
24
+ /**
25
+ * Write a test config file.
26
+ */
27
+ async function writeTestConfig(dir, relativePath, content) {
28
+ const fullPath = path.join(dir, relativePath);
29
+ const dirPath = path.dirname(fullPath);
30
+ await fs.mkdir(dirPath, { recursive: true });
31
+ const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
32
+ await fs.writeFile(fullPath, data, 'utf-8');
33
+ }
34
+ /**
35
+ * Read a test config file.
36
+ */
37
+ async function readTestConfig(dir, relativePath) {
38
+ const fullPath = path.join(dir, relativePath);
39
+ const content = await fs.readFile(fullPath, 'utf-8');
40
+ return JSON.parse(content);
41
+ }
42
+ // =============================================================================
43
+ // Config Generation Tests
44
+ // =============================================================================
45
+ describe('MCP Config Generation', () => {
46
+ it('generateViberagConfig returns correct structure', () => {
47
+ const config = generateViberagConfig();
48
+ expect(config).toEqual({
49
+ command: 'npx',
50
+ args: ['viberag-mcp'],
51
+ });
52
+ });
53
+ it('generateMcpConfig uses correct key for Claude Code', () => {
54
+ const editor = getEditor('claude-code');
55
+ const config = generateMcpConfig(editor);
56
+ expect(config).toHaveProperty('mcpServers');
57
+ expect(config.mcpServers.viberag).toBeDefined();
58
+ });
59
+ it('generateMcpConfig uses "mcp" key for OpenCode', () => {
60
+ const editor = getEditor('opencode');
61
+ const config = generateMcpConfig(editor);
62
+ expect(config).toHaveProperty('mcp');
63
+ expect(config.mcp.viberag).toBeDefined();
64
+ });
65
+ it('generateMcpConfig uses "servers" key for VS Code', () => {
66
+ const editor = getEditor('vscode');
67
+ const config = generateMcpConfig(editor);
68
+ expect(config).toHaveProperty('servers');
69
+ expect(config).not.toHaveProperty('mcpServers');
70
+ });
71
+ it('generateMcpConfig uses "context_servers" key for Zed', () => {
72
+ const editor = getEditor('zed');
73
+ const config = generateMcpConfig(editor);
74
+ expect(config).toHaveProperty('context_servers');
75
+ expect(config).not.toHaveProperty('mcpServers');
76
+ });
77
+ it('generateTomlConfig returns valid TOML', () => {
78
+ const toml = generateTomlConfig();
79
+ expect(toml).toContain('[mcp_servers.viberag]');
80
+ expect(toml).toContain('command = "npx"');
81
+ expect(toml).toContain('args = ["viberag-mcp"]');
82
+ });
83
+ });
84
+ // =============================================================================
85
+ // Project-Level Config Creation Tests
86
+ // =============================================================================
87
+ describe('Project-Level Config Creation', () => {
88
+ let ctx;
89
+ beforeEach(async () => {
90
+ ctx = await createTempDir();
91
+ });
92
+ afterEach(async () => {
93
+ await ctx.cleanup();
94
+ });
95
+ it('creates .mcp.json for Claude Code in empty project', async () => {
96
+ const editor = getEditor('claude-code');
97
+ const result = await writeMcpConfig(editor, ctx.dir);
98
+ expect(result.success).toBe(true);
99
+ expect(result.method).toBe('file-created');
100
+ expect(result.configPath).toBe(path.join(ctx.dir, '.mcp.json'));
101
+ const config = await readTestConfig(ctx.dir, '.mcp.json');
102
+ expect(config.mcpServers.viberag
103
+ .command).toBe('npx');
104
+ });
105
+ it('creates .vscode/mcp.json with parent directory', async () => {
106
+ const editor = getEditor('vscode');
107
+ const result = await writeMcpConfig(editor, ctx.dir);
108
+ expect(result.success).toBe(true);
109
+ expect(result.method).toBe('file-created');
110
+ // Verify .vscode directory was created
111
+ const vscodeDir = path.join(ctx.dir, '.vscode');
112
+ const stat = await fs.stat(vscodeDir);
113
+ expect(stat.isDirectory()).toBe(true);
114
+ const config = await readTestConfig(ctx.dir, '.vscode/mcp.json');
115
+ expect(config.servers.viberag).toBeDefined();
116
+ });
117
+ it('creates .cursor/mcp.json for Cursor', async () => {
118
+ const editor = getEditor('cursor');
119
+ const result = await writeMcpConfig(editor, ctx.dir);
120
+ expect(result.success).toBe(true);
121
+ expect(result.configPath).toContain('.cursor/mcp.json');
122
+ const config = await readTestConfig(ctx.dir, '.cursor/mcp.json');
123
+ expect(config.mcpServers.viberag).toBeDefined();
124
+ });
125
+ it('creates .roo/mcp.json for Roo Code', async () => {
126
+ const editor = getEditor('roo-code');
127
+ const result = await writeMcpConfig(editor, ctx.dir);
128
+ expect(result.success).toBe(true);
129
+ expect(result.configPath).toContain('.roo/mcp.json');
130
+ const config = await readTestConfig(ctx.dir, '.roo/mcp.json');
131
+ expect(config.mcpServers.viberag).toBeDefined();
132
+ });
133
+ });
134
+ // =============================================================================
135
+ // Config Merging Tests
136
+ // =============================================================================
137
+ describe('Config Merging', () => {
138
+ let ctx;
139
+ beforeEach(async () => {
140
+ ctx = await createTempDir();
141
+ });
142
+ afterEach(async () => {
143
+ await ctx.cleanup();
144
+ });
145
+ it('merges viberag into existing mcpServers', async () => {
146
+ // Setup: Create existing config with another server
147
+ await writeTestConfig(ctx.dir, '.mcp.json', {
148
+ mcpServers: {
149
+ 'other-server': {
150
+ command: 'node',
151
+ args: ['other.js'],
152
+ },
153
+ },
154
+ });
155
+ const editor = getEditor('claude-code');
156
+ const result = await writeMcpConfig(editor, ctx.dir);
157
+ expect(result.success).toBe(true);
158
+ expect(result.method).toBe('file-merged');
159
+ const config = (await readTestConfig(ctx.dir, '.mcp.json'));
160
+ // Both servers should exist
161
+ expect(config.mcpServers['other-server']).toBeDefined();
162
+ expect(config.mcpServers.viberag).toBeDefined();
163
+ });
164
+ it('preserves existing servers during merge', async () => {
165
+ const existingConfig = {
166
+ mcpServers: {
167
+ github: { command: 'npx', args: ['github-mcp'] },
168
+ filesystem: { command: 'npx', args: ['fs-mcp'] },
169
+ },
170
+ };
171
+ await writeTestConfig(ctx.dir, '.mcp.json', existingConfig);
172
+ const editor = getEditor('claude-code');
173
+ await writeMcpConfig(editor, ctx.dir);
174
+ const config = (await readTestConfig(ctx.dir, '.mcp.json'));
175
+ expect(Object.keys(config.mcpServers)).toHaveLength(3);
176
+ expect(config.mcpServers['github']).toBeDefined();
177
+ expect(config.mcpServers['filesystem']).toBeDefined();
178
+ expect(config.mcpServers['viberag']).toBeDefined();
179
+ });
180
+ it('merges into VS Code config with servers key', async () => {
181
+ await writeTestConfig(ctx.dir, '.vscode/mcp.json', {
182
+ servers: {
183
+ existing: { command: 'existing' },
184
+ },
185
+ });
186
+ const editor = getEditor('vscode');
187
+ await writeMcpConfig(editor, ctx.dir);
188
+ const config = (await readTestConfig(ctx.dir, '.vscode/mcp.json'));
189
+ expect(config.servers['existing']).toBeDefined();
190
+ expect(config.servers['viberag']).toBeDefined();
191
+ });
192
+ it('mergeConfig function works correctly', () => {
193
+ const existing = {
194
+ mcpServers: {
195
+ other: { command: 'other' },
196
+ },
197
+ someOtherKey: 'value',
198
+ };
199
+ const editor = getEditor('claude-code');
200
+ const merged = mergeConfig(existing, editor);
201
+ expect(merged.mcpServers['other']).toBeDefined();
202
+ expect(merged.mcpServers['viberag']).toBeDefined();
203
+ expect(merged['someOtherKey']).toBe('value');
204
+ });
205
+ it('mergeConfig handles empty servers object', () => {
206
+ const existing = { mcpServers: {} };
207
+ const editor = getEditor('claude-code');
208
+ const merged = mergeConfig(existing, editor);
209
+ expect(merged.mcpServers['viberag']).toBeDefined();
210
+ });
211
+ it('mergeConfig creates servers object if missing', () => {
212
+ const existing = { otherKey: 'value' };
213
+ const editor = getEditor('claude-code');
214
+ const merged = mergeConfig(existing, editor);
215
+ expect(merged.mcpServers['viberag']).toBeDefined();
216
+ });
217
+ it('generateMcpConfig works with OpenCode nested mcp structure', () => {
218
+ const existing = {
219
+ mcp: {
220
+ 'other-server': { command: 'other' },
221
+ },
222
+ someOtherKey: 'value',
223
+ };
224
+ const editor = getEditor('opencode');
225
+ const merged = mergeConfig(existing, editor);
226
+ expect(merged.mcp['other-server']).toBeDefined();
227
+ expect(merged.mcp['viberag']).toBeDefined();
228
+ expect(merged['someOtherKey']).toBe('value');
229
+ });
230
+ });
231
+ // =============================================================================
232
+ // OpenCode-Specific Tests
233
+ // =============================================================================
234
+ describe('OpenCode MCP Configuration', () => {
235
+ it('generateOpenCodeViberagConfig returns correct OpenCode format', () => {
236
+ const config = generateOpenCodeViberagConfig();
237
+ expect(config).toEqual({
238
+ type: 'local',
239
+ command: ['npx', '-y', 'viberag-mcp'],
240
+ });
241
+ });
242
+ it('generateMcpConfig uses OpenCode format for OpenCode editor', () => {
243
+ const editor = getEditor('opencode');
244
+ const config = generateMcpConfig(editor);
245
+ expect(config).toHaveProperty('mcp');
246
+ expect(config.mcp
247
+ .viberag.type).toBe('local');
248
+ expect(config.mcp.viberag.command).toEqual(['npx', '-y', 'viberag-mcp']);
249
+ });
250
+ it('generateMcpConfig does NOT use args key for OpenCode', () => {
251
+ const editor = getEditor('opencode');
252
+ const config = generateMcpConfig(editor);
253
+ const viberagConfig = config.mcp.viberag;
254
+ expect(viberagConfig).not.toHaveProperty('args');
255
+ expect(viberagConfig).toHaveProperty('command');
256
+ });
257
+ it('mergeConfig uses OpenCode format when merging for OpenCode', () => {
258
+ const existing = {
259
+ mcp: {
260
+ 'other-server': { command: 'other' },
261
+ },
262
+ };
263
+ const editor = getEditor('opencode');
264
+ const merged = mergeConfig(existing, editor);
265
+ expect(merged.mcp['viberag']?.type).toBe('local');
266
+ expect(merged.mcp['viberag']?.command).toEqual([
267
+ 'npx',
268
+ '-y',
269
+ 'viberag-mcp',
270
+ ]);
271
+ expect(merged.mcp['other-server']).toBeDefined();
272
+ });
273
+ it('hasViberagConfig detects viberag in OpenCode config', () => {
274
+ const config = {
275
+ mcp: {
276
+ viberag: { type: 'local', command: ['npx', '-y', 'viberag-mcp'] },
277
+ },
278
+ };
279
+ const editor = getEditor('opencode');
280
+ expect(hasViberagConfig(config, editor)).toBe(true);
281
+ });
282
+ it('hasViberagConfig returns false for missing viberag in OpenCode', () => {
283
+ const config = {
284
+ mcp: {
285
+ other: { type: 'local', command: ['npx', 'other'] },
286
+ },
287
+ };
288
+ const editor = getEditor('opencode');
289
+ expect(hasViberagConfig(config, editor)).toBe(false);
290
+ });
291
+ });
292
+ // =============================================================================
293
+ // Already Configured Detection Tests
294
+ // =============================================================================
295
+ describe('Already Configured Detection', () => {
296
+ let ctx;
297
+ beforeEach(async () => {
298
+ ctx = await createTempDir();
299
+ });
300
+ afterEach(async () => {
301
+ await ctx.cleanup();
302
+ });
303
+ it('hasViberagConfig returns true when viberag exists', () => {
304
+ const config = {
305
+ mcpServers: {
306
+ viberag: { command: 'npx', args: ['viberag-mcp'] },
307
+ },
308
+ };
309
+ const editor = getEditor('claude-code');
310
+ expect(hasViberagConfig(config, editor)).toBe(true);
311
+ });
312
+ it('hasViberagConfig returns false when viberag missing', () => {
313
+ const config = {
314
+ mcpServers: {
315
+ other: { command: 'other' },
316
+ },
317
+ };
318
+ const editor = getEditor('claude-code');
319
+ expect(hasViberagConfig(config, editor)).toBe(false);
320
+ });
321
+ it('isAlreadyConfigured detects existing viberag config', async () => {
322
+ await writeTestConfig(ctx.dir, '.mcp.json', {
323
+ mcpServers: {
324
+ viberag: { command: 'npx', args: ['viberag-mcp'] },
325
+ },
326
+ });
327
+ const editor = getEditor('claude-code');
328
+ const result = await isAlreadyConfigured(editor, ctx.dir);
329
+ expect(result).toBe(true);
330
+ });
331
+ it('isAlreadyConfigured returns false for empty project', async () => {
332
+ const editor = getEditor('claude-code');
333
+ const result = await isAlreadyConfigured(editor, ctx.dir);
334
+ expect(result).toBe(false);
335
+ });
336
+ it('writeMcpConfig reports already configured', async () => {
337
+ // Pre-configure viberag
338
+ await writeTestConfig(ctx.dir, '.mcp.json', {
339
+ mcpServers: {
340
+ viberag: { command: 'npx', args: ['viberag-mcp'] },
341
+ },
342
+ });
343
+ const editor = getEditor('claude-code');
344
+ const result = await writeMcpConfig(editor, ctx.dir);
345
+ expect(result.success).toBe(true);
346
+ expect(result.method).toBe('file-merged');
347
+ expect(result.error).toBe('Already configured');
348
+ });
349
+ });
350
+ // =============================================================================
351
+ // Edge Cases Tests
352
+ // =============================================================================
353
+ describe('Edge Cases', () => {
354
+ let ctx;
355
+ beforeEach(async () => {
356
+ ctx = await createTempDir();
357
+ });
358
+ afterEach(async () => {
359
+ await ctx.cleanup();
360
+ });
361
+ it('handles empty existing config file', async () => {
362
+ await writeTestConfig(ctx.dir, '.mcp.json', {});
363
+ const editor = getEditor('claude-code');
364
+ const result = await writeMcpConfig(editor, ctx.dir);
365
+ expect(result.success).toBe(true);
366
+ const config = (await readTestConfig(ctx.dir, '.mcp.json'));
367
+ expect(config.mcpServers['viberag']).toBeDefined();
368
+ });
369
+ it('handles malformed JSON gracefully', async () => {
370
+ // Write invalid JSON
371
+ const configPath = path.join(ctx.dir, '.mcp.json');
372
+ await fs.writeFile(configPath, '{ invalid json }', 'utf-8');
373
+ const editor = getEditor('claude-code');
374
+ const result = await writeMcpConfig(editor, ctx.dir);
375
+ expect(result.success).toBe(false);
376
+ expect(result.error).toContain('parse');
377
+ });
378
+ it('readJsonConfig returns null for non-existent file', async () => {
379
+ const result = await readJsonConfig(path.join(ctx.dir, 'nonexistent.json'));
380
+ expect(result).toBeNull();
381
+ });
382
+ it('configExists returns false for non-existent file', async () => {
383
+ const result = await configExists(path.join(ctx.dir, 'nonexistent.json'));
384
+ expect(result).toBe(false);
385
+ });
386
+ it('configExists returns true for existing file', async () => {
387
+ await writeTestConfig(ctx.dir, 'test.json', {});
388
+ const result = await configExists(path.join(ctx.dir, 'test.json'));
389
+ expect(result).toBe(true);
390
+ });
391
+ });
392
+ // =============================================================================
393
+ // Editor Configuration Data Tests
394
+ // =============================================================================
395
+ describe('Editor Configuration Data', () => {
396
+ it('all project-scope editors have canAutoCreate true', () => {
397
+ const projectEditors = EDITORS.filter(e => e.scope === 'project');
398
+ for (const editor of projectEditors) {
399
+ expect(editor.canAutoCreate).toBe(true);
400
+ }
401
+ });
402
+ it('all editors have valid docsUrl', () => {
403
+ for (const editor of EDITORS) {
404
+ expect(editor.docsUrl).toMatch(/^https?:\/\//);
405
+ }
406
+ });
407
+ it('getEditor returns correct editor by id', () => {
408
+ const claude = getEditor('claude-code');
409
+ expect(claude?.name).toBe('Claude Code');
410
+ const vscode = getEditor('vscode');
411
+ expect(vscode?.name).toBe('VS Code Copilot');
412
+ const nonexistent = getEditor('nonexistent');
413
+ expect(nonexistent).toBeUndefined();
414
+ });
415
+ it('each editor has unique id', () => {
416
+ const ids = EDITORS.map(e => e.id);
417
+ const uniqueIds = new Set(ids);
418
+ expect(uniqueIds.size).toBe(ids.length);
419
+ });
420
+ });
421
+ // =============================================================================
422
+ // Config Removal Tests
423
+ // =============================================================================
424
+ describe('Config Removal', () => {
425
+ let ctx;
426
+ beforeEach(async () => {
427
+ ctx = await createTempDir();
428
+ });
429
+ afterEach(async () => {
430
+ await ctx.cleanup();
431
+ });
432
+ it('removes viberag while preserving other servers', async () => {
433
+ // Setup: config with viberag and another server
434
+ await writeTestConfig(ctx.dir, '.mcp.json', {
435
+ mcpServers: {
436
+ viberag: { command: 'npx', args: ['viberag-mcp'] },
437
+ github: { command: 'npx', args: ['github-mcp'] },
438
+ },
439
+ });
440
+ const editor = getEditor('claude-code');
441
+ const result = await removeViberagConfig(editor, ctx.dir);
442
+ expect(result.success).toBe(true);
443
+ expect(result.editor).toBe('claude-code');
444
+ // File should still exist with github server preserved
445
+ const config = (await readTestConfig(ctx.dir, '.mcp.json'));
446
+ expect(config.mcpServers['github']).toBeDefined();
447
+ expect(config.mcpServers['viberag']).toBeUndefined();
448
+ });
449
+ it('removes viberag and keeps file with empty servers', async () => {
450
+ // Setup: config with only viberag
451
+ await writeTestConfig(ctx.dir, '.mcp.json', {
452
+ mcpServers: {
453
+ viberag: { command: 'npx', args: ['viberag-mcp'] },
454
+ },
455
+ });
456
+ const editor = getEditor('claude-code');
457
+ const result = await removeViberagConfig(editor, ctx.dir);
458
+ expect(result.success).toBe(true);
459
+ // File should still exist with empty mcpServers
460
+ const config = (await readTestConfig(ctx.dir, '.mcp.json'));
461
+ expect(config.mcpServers).toBeDefined();
462
+ expect(Object.keys(config.mcpServers)).toHaveLength(0);
463
+ });
464
+ it('returns error when viberag not configured', async () => {
465
+ // Setup: config without viberag
466
+ await writeTestConfig(ctx.dir, '.mcp.json', {
467
+ mcpServers: {
468
+ github: { command: 'npx', args: ['github-mcp'] },
469
+ },
470
+ });
471
+ const editor = getEditor('claude-code');
472
+ const result = await removeViberagConfig(editor, ctx.dir);
473
+ expect(result.success).toBe(false);
474
+ expect(result.error).toContain('not configured');
475
+ });
476
+ it('returns error when config file does not exist', async () => {
477
+ const editor = getEditor('claude-code');
478
+ const result = await removeViberagConfig(editor, ctx.dir);
479
+ expect(result.success).toBe(false);
480
+ expect(result.error).toContain('does not exist');
481
+ });
482
+ it('handles VS Code config with servers key', async () => {
483
+ await writeTestConfig(ctx.dir, '.vscode/mcp.json', {
484
+ servers: {
485
+ viberag: { command: 'npx', args: ['viberag-mcp'] },
486
+ other: { command: 'other' },
487
+ },
488
+ });
489
+ const editor = getEditor('vscode');
490
+ const result = await removeViberagConfig(editor, ctx.dir);
491
+ expect(result.success).toBe(true);
492
+ const config = (await readTestConfig(ctx.dir, '.vscode/mcp.json'));
493
+ expect(config.servers['other']).toBeDefined();
494
+ expect(config.servers['viberag']).toBeUndefined();
495
+ });
496
+ });
497
+ // =============================================================================
498
+ // removeViberagFromConfig Helper Tests
499
+ // =============================================================================
500
+ describe('removeViberagFromConfig helper', () => {
501
+ it('returns null when servers key is missing', () => {
502
+ const existing = { otherKey: 'value' };
503
+ const editor = getEditor('claude-code');
504
+ const result = removeViberagFromConfig(existing, editor);
505
+ expect(result).toBeNull();
506
+ });
507
+ it('returns null when servers is not an object', () => {
508
+ const existing = { mcpServers: 'not-an-object' };
509
+ const editor = getEditor('claude-code');
510
+ const result = removeViberagFromConfig(existing, editor);
511
+ expect(result).toBeNull();
512
+ });
513
+ it('returns null when viberag not in servers', () => {
514
+ const existing = {
515
+ mcpServers: {
516
+ github: { command: 'github' },
517
+ },
518
+ };
519
+ const editor = getEditor('claude-code');
520
+ const result = removeViberagFromConfig(existing, editor);
521
+ expect(result).toBeNull();
522
+ });
523
+ it('returns modified config with viberag removed', () => {
524
+ const existing = {
525
+ mcpServers: {
526
+ viberag: { command: 'npx' },
527
+ github: { command: 'github' },
528
+ },
529
+ otherKey: 'preserved',
530
+ };
531
+ const editor = getEditor('claude-code');
532
+ const result = removeViberagFromConfig(existing, editor);
533
+ expect(result).not.toBeNull();
534
+ expect(result.mcpServers['viberag']).toBeUndefined();
535
+ expect(result.mcpServers['github']).toBeDefined();
536
+ expect(result.otherKey).toBe('preserved');
537
+ });
538
+ it('works with VS Code servers key', () => {
539
+ const existing = {
540
+ servers: {
541
+ viberag: { command: 'npx' },
542
+ other: { command: 'other' },
543
+ },
544
+ };
545
+ const editor = getEditor('vscode');
546
+ const result = removeViberagFromConfig(existing, editor);
547
+ expect(result).not.toBeNull();
548
+ expect(result.servers['viberag']).toBeUndefined();
549
+ expect(result.servers['other']).toBeDefined();
550
+ });
551
+ });
552
+ // =============================================================================
553
+ // findConfiguredEditors Tests
554
+ // =============================================================================
555
+ describe('findConfiguredEditors', () => {
556
+ let ctx;
557
+ beforeEach(async () => {
558
+ ctx = await createTempDir();
559
+ });
560
+ afterEach(async () => {
561
+ await ctx.cleanup();
562
+ });
563
+ it('finds project-scope editors with viberag configured', async () => {
564
+ // Setup: Claude Code and Cursor both configured
565
+ await writeTestConfig(ctx.dir, '.mcp.json', {
566
+ mcpServers: { viberag: { command: 'npx' } },
567
+ });
568
+ await writeTestConfig(ctx.dir, '.cursor/mcp.json', {
569
+ mcpServers: { viberag: { command: 'npx' } },
570
+ });
571
+ const result = await findConfiguredEditors(ctx.dir);
572
+ expect(result.projectScope.length).toBeGreaterThanOrEqual(2);
573
+ const ids = result.projectScope.map(e => e.id);
574
+ expect(ids).toContain('claude-code');
575
+ expect(ids).toContain('cursor');
576
+ });
577
+ it('returns empty project scope when no configs exist', async () => {
578
+ const result = await findConfiguredEditors(ctx.dir);
579
+ // Project scope should be empty since we didn't create any configs
580
+ expect(result.projectScope).toHaveLength(0);
581
+ // Global scope might contain editors if they're configured globally on this machine
582
+ // This is expected behavior - the function checks real global configs
583
+ });
584
+ it('does not include editors without viberag', async () => {
585
+ // Setup: Claude Code has viberag, Cursor has different server
586
+ await writeTestConfig(ctx.dir, '.mcp.json', {
587
+ mcpServers: { viberag: { command: 'npx' } },
588
+ });
589
+ await writeTestConfig(ctx.dir, '.cursor/mcp.json', {
590
+ mcpServers: { github: { command: 'npx' } },
591
+ });
592
+ const result = await findConfiguredEditors(ctx.dir);
593
+ const ids = result.projectScope.map(e => e.id);
594
+ expect(ids).toContain('claude-code');
595
+ expect(ids).not.toContain('cursor');
596
+ });
597
+ });
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function App(): React.JSX.Element;