@nicnocquee/dataqueue 1.32.0 → 1.34.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,216 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import {
3
+ runInstallMcp,
4
+ upsertMcpConfig,
5
+ InstallMcpDeps,
6
+ } from './install-mcp-command.js';
7
+
8
+ describe('upsertMcpConfig', () => {
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ });
12
+
13
+ it('creates new config file when it does not exist', () => {
14
+ // Setup
15
+ const deps = {
16
+ existsSync: vi.fn(() => false),
17
+ readFileSync: vi.fn(),
18
+ writeFileSync: vi.fn(),
19
+ };
20
+
21
+ // Act
22
+ upsertMcpConfig(
23
+ '/path/mcp.json',
24
+ 'dataqueue',
25
+ { command: 'npx', args: ['dataqueue-cli', 'mcp'] },
26
+ deps,
27
+ );
28
+
29
+ // Assert
30
+ const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
31
+ expect(written.mcpServers.dataqueue).toEqual({
32
+ command: 'npx',
33
+ args: ['dataqueue-cli', 'mcp'],
34
+ });
35
+ });
36
+
37
+ it('adds to existing config without overwriting other servers', () => {
38
+ // Setup
39
+ const existing = JSON.stringify({
40
+ mcpServers: { other: { command: 'other' } },
41
+ });
42
+ const deps = {
43
+ existsSync: vi.fn(() => true),
44
+ readFileSync: vi.fn(() => existing),
45
+ writeFileSync: vi.fn(),
46
+ };
47
+
48
+ // Act
49
+ upsertMcpConfig(
50
+ '/path/mcp.json',
51
+ 'dataqueue',
52
+ { command: 'npx', args: ['dataqueue-cli', 'mcp'] },
53
+ deps,
54
+ );
55
+
56
+ // Assert
57
+ const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
58
+ expect(written.mcpServers.other).toEqual({ command: 'other' });
59
+ expect(written.mcpServers.dataqueue).toEqual({
60
+ command: 'npx',
61
+ args: ['dataqueue-cli', 'mcp'],
62
+ });
63
+ });
64
+
65
+ it('overwrites existing dataqueue entry', () => {
66
+ // Setup
67
+ const existing = JSON.stringify({
68
+ mcpServers: { dataqueue: { command: 'old' } },
69
+ });
70
+ const deps = {
71
+ existsSync: vi.fn(() => true),
72
+ readFileSync: vi.fn(() => existing),
73
+ writeFileSync: vi.fn(),
74
+ };
75
+
76
+ // Act
77
+ upsertMcpConfig('/path/mcp.json', 'dataqueue', { command: 'new' }, deps);
78
+
79
+ // Assert
80
+ const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
81
+ expect(written.mcpServers.dataqueue).toEqual({ command: 'new' });
82
+ });
83
+
84
+ it('handles malformed JSON in existing file', () => {
85
+ // Setup
86
+ const deps = {
87
+ existsSync: vi.fn(() => true),
88
+ readFileSync: vi.fn(() => 'not json'),
89
+ writeFileSync: vi.fn(),
90
+ };
91
+
92
+ // Act
93
+ upsertMcpConfig('/path/mcp.json', 'dataqueue', { command: 'npx' }, deps);
94
+
95
+ // Assert
96
+ const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
97
+ expect(written.mcpServers.dataqueue).toEqual({ command: 'npx' });
98
+ });
99
+ });
100
+
101
+ describe('runInstallMcp', () => {
102
+ afterEach(() => {
103
+ vi.restoreAllMocks();
104
+ });
105
+
106
+ function makeDeps(overrides: Partial<InstallMcpDeps> = {}): InstallMcpDeps {
107
+ return {
108
+ log: vi.fn(),
109
+ error: vi.fn(),
110
+ exit: vi.fn(),
111
+ cwd: '/project',
112
+ readFileSync: vi.fn(() => '{}'),
113
+ writeFileSync: vi.fn(),
114
+ mkdirSync: vi.fn(),
115
+ existsSync: vi.fn(() => false),
116
+ ...overrides,
117
+ };
118
+ }
119
+
120
+ it('installs MCP config for Cursor (option 1)', async () => {
121
+ // Setup
122
+ const deps = makeDeps({ selectedClient: '1' });
123
+
124
+ // Act
125
+ await runInstallMcp(deps);
126
+
127
+ // Assert
128
+ expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.cursor', {
129
+ recursive: true,
130
+ });
131
+ const written = JSON.parse(
132
+ (deps.writeFileSync as ReturnType<typeof vi.fn>).mock
133
+ .calls[0][1] as string,
134
+ );
135
+ expect(written.mcpServers.dataqueue.command).toBe('npx');
136
+ expect(written.mcpServers.dataqueue.args).toEqual(['dataqueue-cli', 'mcp']);
137
+ });
138
+
139
+ it('installs MCP config for Claude Code (option 2)', async () => {
140
+ // Setup
141
+ const deps = makeDeps({ selectedClient: '2' });
142
+
143
+ // Act
144
+ await runInstallMcp(deps);
145
+
146
+ // Assert
147
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
148
+ '/project/.mcp.json',
149
+ expect.any(String),
150
+ );
151
+ });
152
+
153
+ it('installs MCP config for VS Code (option 3)', async () => {
154
+ // Setup
155
+ const deps = makeDeps({ selectedClient: '3' });
156
+
157
+ // Act
158
+ await runInstallMcp(deps);
159
+
160
+ // Assert
161
+ expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.vscode', {
162
+ recursive: true,
163
+ });
164
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
165
+ expect.stringContaining('.vscode/mcp.json'),
166
+ expect.any(String),
167
+ );
168
+ });
169
+
170
+ it('exits with error for invalid choice', async () => {
171
+ // Setup
172
+ const deps = makeDeps({ selectedClient: '99' });
173
+
174
+ // Act
175
+ await runInstallMcp(deps);
176
+
177
+ // Assert
178
+ expect(deps.error).toHaveBeenCalledWith(
179
+ expect.stringContaining('Invalid choice'),
180
+ );
181
+ expect(deps.exit).toHaveBeenCalledWith(1);
182
+ });
183
+
184
+ it('handles install errors', async () => {
185
+ // Setup
186
+ const deps = makeDeps({
187
+ selectedClient: '1',
188
+ writeFileSync: vi.fn(() => {
189
+ throw new Error('permission denied');
190
+ }),
191
+ });
192
+
193
+ // Act
194
+ await runInstallMcp(deps);
195
+
196
+ // Assert
197
+ expect(deps.error).toHaveBeenCalledWith(
198
+ 'Failed to install MCP config:',
199
+ expect.any(Error),
200
+ );
201
+ expect(deps.exit).toHaveBeenCalledWith(1);
202
+ });
203
+
204
+ it('logs done message on success', async () => {
205
+ // Setup
206
+ const deps = makeDeps({ selectedClient: '1' });
207
+
208
+ // Act
209
+ await runInstallMcp(deps);
210
+
211
+ // Assert
212
+ expect(deps.log).toHaveBeenCalledWith(
213
+ expect.stringContaining('npx dataqueue-cli mcp'),
214
+ );
215
+ });
216
+ });
@@ -0,0 +1,185 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+
5
+ export interface InstallMcpDeps {
6
+ log?: (...args: unknown[]) => void;
7
+ error?: (...args: unknown[]) => void;
8
+ exit?: (code: number) => void;
9
+ cwd?: string;
10
+ readFileSync?: (p: string, enc: BufferEncoding) => string;
11
+ writeFileSync?: (p: string, data: string) => void;
12
+ mkdirSync?: (p: string, opts?: fs.MakeDirectoryOptions) => void;
13
+ existsSync?: (p: string) => boolean;
14
+ /** Override for selecting the client (skips interactive prompt). */
15
+ selectedClient?: string;
16
+ }
17
+
18
+ interface McpClientConfig {
19
+ label: string;
20
+ install: (
21
+ deps: Required<
22
+ Pick<
23
+ InstallMcpDeps,
24
+ | 'cwd'
25
+ | 'readFileSync'
26
+ | 'writeFileSync'
27
+ | 'mkdirSync'
28
+ | 'existsSync'
29
+ | 'log'
30
+ >
31
+ >,
32
+ ) => void;
33
+ }
34
+
35
+ /**
36
+ * Merges the dataqueue MCP server config into an existing JSON config file.
37
+ *
38
+ * @param filePath - Path to the MCP config file.
39
+ * @param serverKey - Key name for the server entry.
40
+ * @param serverConfig - Server configuration object.
41
+ * @param deps - Injectable file system functions.
42
+ */
43
+ export function upsertMcpConfig(
44
+ filePath: string,
45
+ serverKey: string,
46
+ serverConfig: Record<string, unknown>,
47
+ deps: {
48
+ readFileSync: (p: string, enc: BufferEncoding) => string;
49
+ writeFileSync: (p: string, data: string) => void;
50
+ existsSync: (p: string) => boolean;
51
+ },
52
+ ): void {
53
+ let config: Record<string, unknown> = {};
54
+
55
+ if (deps.existsSync(filePath)) {
56
+ try {
57
+ config = JSON.parse(deps.readFileSync(filePath, 'utf-8'));
58
+ } catch {
59
+ config = {};
60
+ }
61
+ }
62
+
63
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
64
+ config.mcpServers = {};
65
+ }
66
+
67
+ (config.mcpServers as Record<string, unknown>)[serverKey] = serverConfig;
68
+ deps.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
69
+ }
70
+
71
+ const MCP_SERVER_CONFIG = {
72
+ command: 'npx',
73
+ args: ['dataqueue-cli', 'mcp'],
74
+ };
75
+
76
+ const MCP_CLIENTS: Record<string, McpClientConfig> = {
77
+ '1': {
78
+ label: 'Cursor',
79
+ install: (deps) => {
80
+ const configDir = path.join(deps.cwd, '.cursor');
81
+ deps.mkdirSync(configDir, { recursive: true });
82
+ const configFile = path.join(configDir, 'mcp.json');
83
+ upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
84
+ deps.log(` ✓ .cursor/mcp.json`);
85
+ },
86
+ },
87
+ '2': {
88
+ label: 'Claude Code',
89
+ install: (deps) => {
90
+ const configFile = path.join(deps.cwd, '.mcp.json');
91
+ upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
92
+ deps.log(` ✓ .mcp.json`);
93
+ },
94
+ },
95
+ '3': {
96
+ label: 'VS Code (Copilot)',
97
+ install: (deps) => {
98
+ const configDir = path.join(deps.cwd, '.vscode');
99
+ deps.mkdirSync(configDir, { recursive: true });
100
+ const configFile = path.join(configDir, 'mcp.json');
101
+ upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
102
+ deps.log(` ✓ .vscode/mcp.json`);
103
+ },
104
+ },
105
+ '4': {
106
+ label: 'Windsurf',
107
+ install: (deps) => {
108
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
109
+ const configFile = path.join(
110
+ homeDir,
111
+ '.codeium',
112
+ 'windsurf',
113
+ 'mcp_config.json',
114
+ );
115
+ deps.mkdirSync(path.dirname(configFile), { recursive: true });
116
+ upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
117
+ deps.log(` ✓ ~/.codeium/windsurf/mcp_config.json`);
118
+ },
119
+ },
120
+ };
121
+
122
+ /**
123
+ * Installs the DataQueue MCP server config for the selected AI client.
124
+ *
125
+ * @param deps - Injectable dependencies for testing.
126
+ */
127
+ export async function runInstallMcp({
128
+ log = console.log,
129
+ error = console.error,
130
+ exit = (code: number) => process.exit(code),
131
+ cwd = process.cwd(),
132
+ readFileSync = fs.readFileSync,
133
+ writeFileSync = fs.writeFileSync,
134
+ mkdirSync = fs.mkdirSync,
135
+ existsSync = fs.existsSync,
136
+ selectedClient,
137
+ }: InstallMcpDeps = {}): Promise<void> {
138
+ log('DataQueue MCP Server Installer\n');
139
+ log('Select your AI client:\n');
140
+
141
+ for (const [key, client] of Object.entries(MCP_CLIENTS)) {
142
+ log(` ${key}) ${client.label}`);
143
+ }
144
+ log('');
145
+
146
+ let choice = selectedClient;
147
+
148
+ if (!choice) {
149
+ const rl = readline.createInterface({
150
+ input: process.stdin,
151
+ output: process.stdout,
152
+ });
153
+
154
+ choice = await new Promise<string>((resolve) => {
155
+ rl.question('Enter choice (1-4): ', (answer) => {
156
+ rl.close();
157
+ resolve(answer.trim());
158
+ });
159
+ });
160
+ }
161
+
162
+ const client = MCP_CLIENTS[choice];
163
+ if (!client) {
164
+ error(`Invalid choice: "${choice}". Expected 1-4.`);
165
+ exit(1);
166
+ return;
167
+ }
168
+
169
+ log(`\nInstalling MCP config for ${client.label}...`);
170
+
171
+ try {
172
+ client.install({
173
+ cwd,
174
+ readFileSync,
175
+ writeFileSync,
176
+ mkdirSync,
177
+ existsSync,
178
+ log,
179
+ });
180
+ log('\nDone! The MCP server will run via: npx dataqueue-cli mcp');
181
+ } catch (err) {
182
+ error('Failed to install MCP config:', err);
183
+ exit(1);
184
+ }
185
+ }
@@ -0,0 +1,218 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import {
3
+ runInstallRules,
4
+ upsertMarkedSection,
5
+ InstallRulesDeps,
6
+ } from './install-rules-command.js';
7
+
8
+ describe('upsertMarkedSection', () => {
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ });
12
+
13
+ it('creates file with markers when file does not exist', () => {
14
+ // Setup
15
+ const deps = {
16
+ existsSync: vi.fn(() => false),
17
+ readFileSync: vi.fn(),
18
+ writeFileSync: vi.fn(),
19
+ };
20
+
21
+ // Act
22
+ upsertMarkedSection('/path/file.md', 'content here', deps);
23
+
24
+ // Assert
25
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
26
+ '/path/file.md',
27
+ expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
28
+ );
29
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
30
+ '/path/file.md',
31
+ expect.stringContaining('content here'),
32
+ );
33
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
34
+ '/path/file.md',
35
+ expect.stringContaining('<!-- DATAQUEUE RULES END -->'),
36
+ );
37
+ });
38
+
39
+ it('replaces existing marked section', () => {
40
+ // Setup
41
+ const existing =
42
+ 'before\n<!-- DATAQUEUE RULES START -->\nold content\n<!-- DATAQUEUE RULES END -->\nafter';
43
+ const deps = {
44
+ existsSync: vi.fn(() => true),
45
+ readFileSync: vi.fn(() => existing),
46
+ writeFileSync: vi.fn(),
47
+ };
48
+
49
+ // Act
50
+ upsertMarkedSection('/path/file.md', 'new content', deps);
51
+
52
+ // Assert
53
+ const written = deps.writeFileSync.mock.calls[0][1] as string;
54
+ expect(written).toContain('before\n');
55
+ expect(written).toContain('new content');
56
+ expect(written).not.toContain('old content');
57
+ expect(written).toContain('\nafter');
58
+ });
59
+
60
+ it('appends to file when no markers exist', () => {
61
+ // Setup
62
+ const deps = {
63
+ existsSync: vi.fn(() => true),
64
+ readFileSync: vi.fn(() => '# Existing content'),
65
+ writeFileSync: vi.fn(),
66
+ };
67
+
68
+ // Act
69
+ upsertMarkedSection('/path/file.md', 'new content', deps);
70
+
71
+ // Assert
72
+ const written = deps.writeFileSync.mock.calls[0][1] as string;
73
+ expect(written).toContain('# Existing content');
74
+ expect(written).toContain('new content');
75
+ });
76
+ });
77
+
78
+ describe('runInstallRules', () => {
79
+ afterEach(() => {
80
+ vi.restoreAllMocks();
81
+ });
82
+
83
+ function makeDeps(
84
+ overrides: Partial<InstallRulesDeps> = {},
85
+ ): InstallRulesDeps {
86
+ return {
87
+ log: vi.fn(),
88
+ error: vi.fn(),
89
+ exit: vi.fn(),
90
+ cwd: '/project',
91
+ readFileSync: vi.fn(() => '# Rule content'),
92
+ writeFileSync: vi.fn(),
93
+ appendFileSync: vi.fn(),
94
+ mkdirSync: vi.fn(),
95
+ existsSync: vi.fn(() => false),
96
+ rulesSourceDir: '/pkg/ai/rules',
97
+ ...overrides,
98
+ };
99
+ }
100
+
101
+ it('installs rules for Cursor (option 1)', async () => {
102
+ // Setup
103
+ const deps = makeDeps({ selectedClient: '1' });
104
+
105
+ // Act
106
+ await runInstallRules(deps);
107
+
108
+ // Assert
109
+ expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.cursor/rules', {
110
+ recursive: true,
111
+ });
112
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
113
+ expect.stringContaining('.cursor/rules/dataqueue-basic.mdc'),
114
+ expect.any(String),
115
+ );
116
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
117
+ expect.stringContaining('.cursor/rules/dataqueue-advanced.mdc'),
118
+ expect.any(String),
119
+ );
120
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
121
+ expect.stringContaining('.cursor/rules/dataqueue-react-dashboard.mdc'),
122
+ expect.any(String),
123
+ );
124
+ });
125
+
126
+ it('installs rules for Claude Code (option 2)', async () => {
127
+ // Setup
128
+ const deps = makeDeps({ selectedClient: '2' });
129
+
130
+ // Act
131
+ await runInstallRules(deps);
132
+
133
+ // Assert
134
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
135
+ '/project/CLAUDE.md',
136
+ expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
137
+ );
138
+ });
139
+
140
+ it('installs rules for AGENTS.md (option 3)', async () => {
141
+ // Setup
142
+ const deps = makeDeps({ selectedClient: '3' });
143
+
144
+ // Act
145
+ await runInstallRules(deps);
146
+
147
+ // Assert
148
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
149
+ '/project/AGENTS.md',
150
+ expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
151
+ );
152
+ });
153
+
154
+ it('installs rules for GitHub Copilot (option 4)', async () => {
155
+ // Setup
156
+ const deps = makeDeps({ selectedClient: '4' });
157
+
158
+ // Act
159
+ await runInstallRules(deps);
160
+
161
+ // Assert
162
+ expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.github', {
163
+ recursive: true,
164
+ });
165
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
166
+ '/project/.github/copilot-instructions.md',
167
+ expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
168
+ );
169
+ });
170
+
171
+ it('installs rules for Windsurf (option 5)', async () => {
172
+ // Setup
173
+ const deps = makeDeps({ selectedClient: '5' });
174
+
175
+ // Act
176
+ await runInstallRules(deps);
177
+
178
+ // Assert
179
+ expect(deps.writeFileSync).toHaveBeenCalledWith(
180
+ '/project/CONVENTIONS.md',
181
+ expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
182
+ );
183
+ });
184
+
185
+ it('exits with error for invalid choice', async () => {
186
+ // Setup
187
+ const deps = makeDeps({ selectedClient: '99' });
188
+
189
+ // Act
190
+ await runInstallRules(deps);
191
+
192
+ // Assert
193
+ expect(deps.error).toHaveBeenCalledWith(
194
+ expect.stringContaining('Invalid choice'),
195
+ );
196
+ expect(deps.exit).toHaveBeenCalledWith(1);
197
+ });
198
+
199
+ it('handles install errors', async () => {
200
+ // Setup
201
+ const deps = makeDeps({
202
+ selectedClient: '1',
203
+ readFileSync: vi.fn(() => {
204
+ throw new Error('file not found');
205
+ }),
206
+ });
207
+
208
+ // Act
209
+ await runInstallRules(deps);
210
+
211
+ // Assert
212
+ expect(deps.error).toHaveBeenCalledWith(
213
+ 'Failed to install rules:',
214
+ expect.any(Error),
215
+ );
216
+ expect(deps.exit).toHaveBeenCalledWith(1);
217
+ });
218
+ });