@nicnocquee/dataqueue 1.33.0 → 1.35.0-beta.20260224075710
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/ai/build-docs-content.ts +96 -0
- package/ai/build-llms-full.ts +42 -0
- package/ai/docs-content.json +290 -0
- package/ai/rules/advanced.md +170 -0
- package/ai/rules/basic.md +159 -0
- package/ai/rules/react-dashboard.md +87 -0
- package/ai/skills/dataqueue-advanced/SKILL.md +370 -0
- package/ai/skills/dataqueue-core/SKILL.md +235 -0
- package/ai/skills/dataqueue-react/SKILL.md +201 -0
- package/dist/cli.cjs +577 -32
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +52 -2
- package/dist/cli.d.ts +52 -2
- package/dist/cli.js +575 -32
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +937 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +358 -11
- package/dist/index.d.ts +358 -11
- package/dist/index.js +937 -108
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +186 -0
- package/dist/mcp-server.cjs.map +1 -0
- package/dist/mcp-server.d.cts +32 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.js +175 -0
- package/dist/mcp-server.js.map +1 -0
- package/migrations/1781200000005_add_retry_config_to_job_queue.sql +17 -0
- package/migrations/1781200000006_add_output_to_job_queue.sql +3 -0
- package/package.json +10 -4
- package/src/backend.ts +36 -3
- package/src/backends/postgres.ts +344 -42
- package/src/backends/redis-scripts.ts +173 -8
- package/src/backends/redis.test.ts +668 -0
- package/src/backends/redis.ts +244 -15
- package/src/cli.test.ts +65 -0
- package/src/cli.ts +56 -19
- package/src/db-util.ts +1 -1
- package/src/index.test.ts +811 -12
- package/src/index.ts +106 -14
- package/src/install-mcp-command.test.ts +216 -0
- package/src/install-mcp-command.ts +185 -0
- package/src/install-rules-command.test.ts +218 -0
- package/src/install-rules-command.ts +233 -0
- package/src/install-skills-command.test.ts +176 -0
- package/src/install-skills-command.ts +124 -0
- package/src/mcp-server.test.ts +162 -0
- package/src/mcp-server.ts +231 -0
- package/src/processor.ts +133 -49
- package/src/queue.test.ts +477 -0
- package/src/queue.ts +20 -3
- package/src/supervisor.test.ts +340 -0
- package/src/supervisor.ts +177 -0
- package/src/types.ts +318 -3
package/src/index.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
1
2
|
import { createProcessor } from './processor.js';
|
|
3
|
+
import { createSupervisor } from './supervisor.js';
|
|
2
4
|
import {
|
|
3
5
|
JobQueueConfig,
|
|
4
6
|
JobQueue,
|
|
5
7
|
JobOptions,
|
|
8
|
+
AddJobOptions,
|
|
6
9
|
ProcessorOptions,
|
|
10
|
+
SupervisorOptions,
|
|
7
11
|
JobHandlers,
|
|
8
12
|
JobType,
|
|
9
13
|
PostgresJobQueueConfig,
|
|
@@ -11,6 +15,9 @@ import {
|
|
|
11
15
|
CronScheduleOptions,
|
|
12
16
|
CronScheduleStatus,
|
|
13
17
|
EditCronScheduleOptions,
|
|
18
|
+
QueueEventMap,
|
|
19
|
+
QueueEventName,
|
|
20
|
+
QueueEmitFn,
|
|
14
21
|
} from './types.js';
|
|
15
22
|
import { QueueBackend, CronScheduleInput } from './backend.js';
|
|
16
23
|
import { setLogContext } from './log-context.js';
|
|
@@ -23,6 +30,8 @@ import { getNextCronOccurrence, validateCronExpression } from './cron.js';
|
|
|
23
30
|
* Initialize the job queue system.
|
|
24
31
|
*
|
|
25
32
|
* Defaults to PostgreSQL when `backend` is omitted.
|
|
33
|
+
* For PostgreSQL, provide either `databaseConfig` or `pool` (bring your own).
|
|
34
|
+
* For Redis, provide either `redisConfig` or `client` (bring your own).
|
|
26
35
|
*/
|
|
27
36
|
export const initJobQueue = <PayloadMap = any>(
|
|
28
37
|
config: JobQueueConfig,
|
|
@@ -34,15 +43,39 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
34
43
|
|
|
35
44
|
if (backendType === 'postgres') {
|
|
36
45
|
const pgConfig = config as PostgresJobQueueConfig;
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
if (pgConfig.pool) {
|
|
47
|
+
backend = new PostgresBackend(pgConfig.pool);
|
|
48
|
+
} else if (pgConfig.databaseConfig) {
|
|
49
|
+
const pool = createPool(pgConfig.databaseConfig);
|
|
50
|
+
backend = new PostgresBackend(pool);
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'PostgreSQL backend requires either "databaseConfig" or "pool" to be provided.',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
39
56
|
} else if (backendType === 'redis') {
|
|
40
|
-
const redisConfig =
|
|
41
|
-
|
|
57
|
+
const redisConfig = config as RedisJobQueueConfig;
|
|
58
|
+
if (redisConfig.client) {
|
|
59
|
+
backend = new RedisBackend(
|
|
60
|
+
redisConfig.client as any,
|
|
61
|
+
redisConfig.keyPrefix,
|
|
62
|
+
);
|
|
63
|
+
} else if (redisConfig.redisConfig) {
|
|
64
|
+
backend = new RedisBackend(redisConfig.redisConfig);
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'Redis backend requires either "redisConfig" or "client" to be provided.',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
42
70
|
} else {
|
|
43
71
|
throw new Error(`Unknown backend: ${backendType}`);
|
|
44
72
|
}
|
|
45
73
|
|
|
74
|
+
const emitter = new EventEmitter();
|
|
75
|
+
const emit: QueueEmitFn = (event, data) => {
|
|
76
|
+
emitter.emit(event, data);
|
|
77
|
+
};
|
|
78
|
+
|
|
46
79
|
/**
|
|
47
80
|
* Enqueue due cron jobs. Shared by the public API and the processor hook.
|
|
48
81
|
*/
|
|
@@ -84,6 +117,9 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
84
117
|
timeoutMs: schedule.timeoutMs ?? undefined,
|
|
85
118
|
forceKillOnTimeout: schedule.forceKillOnTimeout,
|
|
86
119
|
tags: schedule.tags,
|
|
120
|
+
retryDelay: schedule.retryDelay ?? undefined,
|
|
121
|
+
retryBackoff: schedule.retryBackoff ?? undefined,
|
|
122
|
+
retryDelayMax: schedule.retryDelayMax ?? undefined,
|
|
87
123
|
});
|
|
88
124
|
|
|
89
125
|
// Advance to next occurrence
|
|
@@ -107,8 +143,21 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
107
143
|
return {
|
|
108
144
|
// Job queue operations
|
|
109
145
|
addJob: withLogContext(
|
|
110
|
-
(job: JobOptions<PayloadMap, any
|
|
111
|
-
backend.addJob<PayloadMap, any>(job)
|
|
146
|
+
async (job: JobOptions<PayloadMap, any>, options?: AddJobOptions) => {
|
|
147
|
+
const jobId = await backend.addJob<PayloadMap, any>(job, options);
|
|
148
|
+
emit('job:added', { jobId, jobType: job.jobType });
|
|
149
|
+
return jobId;
|
|
150
|
+
},
|
|
151
|
+
config.verbose ?? false,
|
|
152
|
+
),
|
|
153
|
+
addJobs: withLogContext(
|
|
154
|
+
async (jobs: JobOptions<PayloadMap, any>[], options?: AddJobOptions) => {
|
|
155
|
+
const jobIds = await backend.addJobs<PayloadMap, any>(jobs, options);
|
|
156
|
+
for (let i = 0; i < jobIds.length; i++) {
|
|
157
|
+
emit('job:added', { jobId: jobIds[i], jobType: jobs[i].jobType });
|
|
158
|
+
}
|
|
159
|
+
return jobIds;
|
|
160
|
+
},
|
|
112
161
|
config.verbose ?? false,
|
|
113
162
|
),
|
|
114
163
|
getJob: withLogContext(
|
|
@@ -140,15 +189,18 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
140
189
|
) => backend.getJobs<PayloadMap, any>(filters, limit, offset),
|
|
141
190
|
config.verbose ?? false,
|
|
142
191
|
),
|
|
143
|
-
retryJob: (jobId: number) =>
|
|
192
|
+
retryJob: async (jobId: number) => {
|
|
193
|
+
await backend.retryJob(jobId);
|
|
194
|
+
emit('job:retried', { jobId });
|
|
195
|
+
},
|
|
144
196
|
cleanupOldJobs: (daysToKeep?: number, batchSize?: number) =>
|
|
145
197
|
backend.cleanupOldJobs(daysToKeep, batchSize),
|
|
146
198
|
cleanupOldJobEvents: (daysToKeep?: number, batchSize?: number) =>
|
|
147
199
|
backend.cleanupOldJobEvents(daysToKeep, batchSize),
|
|
148
|
-
cancelJob: withLogContext(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
),
|
|
200
|
+
cancelJob: withLogContext(async (jobId: number) => {
|
|
201
|
+
await backend.cancelJob(jobId);
|
|
202
|
+
emit('job:cancelled', { jobId });
|
|
203
|
+
}, config.verbose ?? false),
|
|
152
204
|
editJob: withLogContext(
|
|
153
205
|
<T extends JobType<PayloadMap>>(
|
|
154
206
|
jobId: number,
|
|
@@ -206,9 +258,19 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
206
258
|
handlers: JobHandlers<PayloadMap>,
|
|
207
259
|
options?: ProcessorOptions,
|
|
208
260
|
) =>
|
|
209
|
-
createProcessor<PayloadMap>(
|
|
210
|
-
|
|
211
|
-
|
|
261
|
+
createProcessor<PayloadMap>(
|
|
262
|
+
backend,
|
|
263
|
+
handlers,
|
|
264
|
+
options,
|
|
265
|
+
async () => {
|
|
266
|
+
await enqueueDueCronJobsImpl();
|
|
267
|
+
},
|
|
268
|
+
emit,
|
|
269
|
+
),
|
|
270
|
+
|
|
271
|
+
// Background supervisor — automated maintenance
|
|
272
|
+
createSupervisor: (options?: SupervisorOptions) =>
|
|
273
|
+
createSupervisor(backend, options, emit),
|
|
212
274
|
|
|
213
275
|
// Job events
|
|
214
276
|
getJobEvents: withLogContext(
|
|
@@ -262,6 +324,9 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
262
324
|
timezone: options.timezone ?? 'UTC',
|
|
263
325
|
allowOverlap: options.allowOverlap ?? false,
|
|
264
326
|
nextRunAt,
|
|
327
|
+
retryDelay: options.retryDelay ?? null,
|
|
328
|
+
retryBackoff: options.retryBackoff ?? null,
|
|
329
|
+
retryDelayMax: options.retryDelayMax ?? null,
|
|
265
330
|
};
|
|
266
331
|
return backend.addCronSchedule(input);
|
|
267
332
|
},
|
|
@@ -320,6 +385,33 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
320
385
|
config.verbose ?? false,
|
|
321
386
|
),
|
|
322
387
|
|
|
388
|
+
// Event hooks
|
|
389
|
+
on: <K extends QueueEventName>(
|
|
390
|
+
event: K,
|
|
391
|
+
listener: (data: QueueEventMap[K]) => void,
|
|
392
|
+
) => {
|
|
393
|
+
emitter.on(event, listener as (...args: any[]) => void);
|
|
394
|
+
},
|
|
395
|
+
once: <K extends QueueEventName>(
|
|
396
|
+
event: K,
|
|
397
|
+
listener: (data: QueueEventMap[K]) => void,
|
|
398
|
+
) => {
|
|
399
|
+
emitter.once(event, listener as (...args: any[]) => void);
|
|
400
|
+
},
|
|
401
|
+
off: <K extends QueueEventName>(
|
|
402
|
+
event: K,
|
|
403
|
+
listener: (data: QueueEventMap[K]) => void,
|
|
404
|
+
) => {
|
|
405
|
+
emitter.off(event, listener as (...args: any[]) => void);
|
|
406
|
+
},
|
|
407
|
+
removeAllListeners: (event?: QueueEventName) => {
|
|
408
|
+
if (event) {
|
|
409
|
+
emitter.removeAllListeners(event);
|
|
410
|
+
} else {
|
|
411
|
+
emitter.removeAllListeners();
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
|
|
323
415
|
// Advanced access
|
|
324
416
|
getPool: () => {
|
|
325
417
|
if (!(backend instanceof PostgresBackend)) {
|
|
@@ -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
|
+
}
|