@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/backends/redis.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
EditCronScheduleOptions,
|
|
15
15
|
WaitpointRecord,
|
|
16
16
|
CreateTokenOptions,
|
|
17
|
+
AddJobOptions,
|
|
17
18
|
} from '../types.js';
|
|
18
19
|
import {
|
|
19
20
|
QueueBackend,
|
|
@@ -61,6 +62,7 @@ function parseTimeoutString(timeout: string): number {
|
|
|
61
62
|
}
|
|
62
63
|
import {
|
|
63
64
|
ADD_JOB_SCRIPT,
|
|
65
|
+
ADD_JOBS_SCRIPT,
|
|
64
66
|
GET_NEXT_BATCH_SCRIPT,
|
|
65
67
|
COMPLETE_JOB_SCRIPT,
|
|
66
68
|
FAIL_JOB_SCRIPT,
|
|
@@ -162,9 +164,28 @@ function deserializeJob<PayloadMap, T extends JobType<PayloadMap>>(
|
|
|
162
164
|
waitUntil: dateOrNull(h.waitUntil),
|
|
163
165
|
waitTokenId: nullish(h.waitTokenId) as string | null | undefined,
|
|
164
166
|
stepData: parseStepData(h.stepData),
|
|
167
|
+
retryDelay: numOrNull(h.retryDelay),
|
|
168
|
+
retryBackoff:
|
|
169
|
+
h.retryBackoff === 'true'
|
|
170
|
+
? true
|
|
171
|
+
: h.retryBackoff === 'false'
|
|
172
|
+
? false
|
|
173
|
+
: null,
|
|
174
|
+
retryDelayMax: numOrNull(h.retryDelayMax),
|
|
175
|
+
output: parseJsonField(h.output),
|
|
165
176
|
};
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
/** Parse a JSON field from a Redis hash, returning null for missing/null values. */
|
|
180
|
+
function parseJsonField(raw: string | undefined): unknown {
|
|
181
|
+
if (!raw || raw === 'null') return null;
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(raw);
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
168
189
|
/** Parse step data from a Redis hash field. */
|
|
169
190
|
function parseStepData(
|
|
170
191
|
raw: string | undefined,
|
|
@@ -181,8 +202,30 @@ export class RedisBackend implements QueueBackend {
|
|
|
181
202
|
private client: RedisType;
|
|
182
203
|
private prefix: string;
|
|
183
204
|
|
|
184
|
-
|
|
185
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Create a RedisBackend.
|
|
207
|
+
*
|
|
208
|
+
* @param configOrClient - Either `redisConfig` from the config file (the
|
|
209
|
+
* library creates a new ioredis client) or an existing ioredis client
|
|
210
|
+
* instance (bring your own).
|
|
211
|
+
* @param keyPrefix - Key prefix, only used when `configOrClient` is an
|
|
212
|
+
* external client. Ignored when `redisConfig` is passed (uses
|
|
213
|
+
* `redisConfig.keyPrefix` instead). Default: `'dq:'`.
|
|
214
|
+
*/
|
|
215
|
+
constructor(
|
|
216
|
+
configOrClient: RedisJobQueueConfig['redisConfig'] | RedisType,
|
|
217
|
+
keyPrefix?: string,
|
|
218
|
+
) {
|
|
219
|
+
if (configOrClient && typeof (configOrClient as any).eval === 'function') {
|
|
220
|
+
this.client = configOrClient as RedisType;
|
|
221
|
+
this.prefix = keyPrefix ?? 'dq:';
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const redisConfig = configOrClient as NonNullable<
|
|
226
|
+
RedisJobQueueConfig['redisConfig']
|
|
227
|
+
>;
|
|
228
|
+
|
|
186
229
|
let IORedis: any;
|
|
187
230
|
try {
|
|
188
231
|
const _require = createRequire(import.meta.url);
|
|
@@ -260,17 +303,29 @@ export class RedisBackend implements QueueBackend {
|
|
|
260
303
|
|
|
261
304
|
// ── Job CRUD ──────────────────────────────────────────────────────────
|
|
262
305
|
|
|
263
|
-
async addJob<PayloadMap, T extends JobType<PayloadMap>>(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
306
|
+
async addJob<PayloadMap, T extends JobType<PayloadMap>>(
|
|
307
|
+
{
|
|
308
|
+
jobType,
|
|
309
|
+
payload,
|
|
310
|
+
maxAttempts = 3,
|
|
311
|
+
priority = 0,
|
|
312
|
+
runAt = null,
|
|
313
|
+
timeoutMs = undefined,
|
|
314
|
+
forceKillOnTimeout = false,
|
|
315
|
+
tags = undefined,
|
|
316
|
+
idempotencyKey = undefined,
|
|
317
|
+
retryDelay = undefined,
|
|
318
|
+
retryBackoff = undefined,
|
|
319
|
+
retryDelayMax = undefined,
|
|
320
|
+
}: JobOptions<PayloadMap, T>,
|
|
321
|
+
options?: AddJobOptions,
|
|
322
|
+
): Promise<number> {
|
|
323
|
+
if (options?.db) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
'The db option is not supported with the Redis backend. ' +
|
|
326
|
+
'Transactional job creation is only available with PostgreSQL.',
|
|
327
|
+
);
|
|
328
|
+
}
|
|
274
329
|
const now = this.nowMs();
|
|
275
330
|
const runAtMs = runAt ? runAt.getTime() : 0;
|
|
276
331
|
|
|
@@ -288,6 +343,9 @@ export class RedisBackend implements QueueBackend {
|
|
|
288
343
|
tags ? JSON.stringify(tags) : 'null',
|
|
289
344
|
idempotencyKey ?? 'null',
|
|
290
345
|
now,
|
|
346
|
+
retryDelay !== undefined ? retryDelay.toString() : 'null',
|
|
347
|
+
retryBackoff !== undefined ? retryBackoff.toString() : 'null',
|
|
348
|
+
retryDelayMax !== undefined ? retryDelayMax.toString() : 'null',
|
|
291
349
|
)) as number;
|
|
292
350
|
|
|
293
351
|
const jobId = Number(result);
|
|
@@ -303,6 +361,83 @@ export class RedisBackend implements QueueBackend {
|
|
|
303
361
|
return jobId;
|
|
304
362
|
}
|
|
305
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Insert multiple jobs atomically via a single Lua script.
|
|
366
|
+
* Returns IDs in the same order as the input array.
|
|
367
|
+
*/
|
|
368
|
+
async addJobs<PayloadMap, T extends JobType<PayloadMap>>(
|
|
369
|
+
jobs: JobOptions<PayloadMap, T>[],
|
|
370
|
+
options?: AddJobOptions,
|
|
371
|
+
): Promise<number[]> {
|
|
372
|
+
if (jobs.length === 0) return [];
|
|
373
|
+
|
|
374
|
+
if (options?.db) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
'The db option is not supported with the Redis backend. ' +
|
|
377
|
+
'Transactional job creation is only available with PostgreSQL.',
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const now = this.nowMs();
|
|
382
|
+
|
|
383
|
+
const jobsPayload = jobs.map((job) => ({
|
|
384
|
+
jobType: job.jobType,
|
|
385
|
+
payload: JSON.stringify(job.payload),
|
|
386
|
+
maxAttempts: job.maxAttempts ?? 3,
|
|
387
|
+
priority: job.priority ?? 0,
|
|
388
|
+
runAtMs: job.runAt ? job.runAt.getTime() : 0,
|
|
389
|
+
timeoutMs:
|
|
390
|
+
job.timeoutMs !== undefined ? job.timeoutMs.toString() : 'null',
|
|
391
|
+
forceKillOnTimeout: job.forceKillOnTimeout ? 'true' : 'false',
|
|
392
|
+
tags: job.tags ? JSON.stringify(job.tags) : 'null',
|
|
393
|
+
idempotencyKey: job.idempotencyKey ?? 'null',
|
|
394
|
+
retryDelay:
|
|
395
|
+
job.retryDelay !== undefined ? job.retryDelay.toString() : 'null',
|
|
396
|
+
retryBackoff:
|
|
397
|
+
job.retryBackoff !== undefined ? job.retryBackoff.toString() : 'null',
|
|
398
|
+
retryDelayMax:
|
|
399
|
+
job.retryDelayMax !== undefined ? job.retryDelayMax.toString() : 'null',
|
|
400
|
+
}));
|
|
401
|
+
|
|
402
|
+
const result = (await this.client.eval(
|
|
403
|
+
ADD_JOBS_SCRIPT,
|
|
404
|
+
1,
|
|
405
|
+
this.prefix,
|
|
406
|
+
JSON.stringify(jobsPayload),
|
|
407
|
+
now,
|
|
408
|
+
)) as number[];
|
|
409
|
+
|
|
410
|
+
const ids = result.map(Number);
|
|
411
|
+
log(`Batch-inserted ${jobs.length} jobs, IDs: [${ids.join(', ')}]`);
|
|
412
|
+
|
|
413
|
+
// Record events for newly inserted jobs (skip idempotency duplicates)
|
|
414
|
+
const existingIdempotencyIds = new Set<number>();
|
|
415
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
416
|
+
if (jobs[i].idempotencyKey) {
|
|
417
|
+
// If the returned ID existed before this batch, it was a duplicate.
|
|
418
|
+
// We detect this by checking if the same ID appears for a different
|
|
419
|
+
// idempotency-keyed job (unlikely) or by checking if the ID was less
|
|
420
|
+
// than what we'd expect. The simplest approach: record events for all,
|
|
421
|
+
// since the Lua script returns the existing ID for duplicates but
|
|
422
|
+
// doesn't tell us if it was newly created. We can compare: if
|
|
423
|
+
// multiple jobs have the same idempotency key in the batch and got
|
|
424
|
+
// the same ID, only record once.
|
|
425
|
+
if (existingIdempotencyIds.has(ids[i])) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
existingIdempotencyIds.add(ids[i]);
|
|
429
|
+
}
|
|
430
|
+
await this.recordJobEvent(ids[i], JobEventType.Added, {
|
|
431
|
+
jobType: jobs[i].jobType,
|
|
432
|
+
payload: jobs[i].payload,
|
|
433
|
+
tags: jobs[i].tags,
|
|
434
|
+
idempotencyKey: jobs[i].idempotencyKey,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return ids;
|
|
439
|
+
}
|
|
440
|
+
|
|
306
441
|
async getJob<PayloadMap, T extends JobType<PayloadMap>>(
|
|
307
442
|
id: number,
|
|
308
443
|
): Promise<JobRecord<PayloadMap, T> | null> {
|
|
@@ -469,9 +604,18 @@ export class RedisBackend implements QueueBackend {
|
|
|
469
604
|
return jobs;
|
|
470
605
|
}
|
|
471
606
|
|
|
472
|
-
async completeJob(jobId: number): Promise<void> {
|
|
607
|
+
async completeJob(jobId: number, output?: unknown): Promise<void> {
|
|
473
608
|
const now = this.nowMs();
|
|
474
|
-
|
|
609
|
+
const outputArg =
|
|
610
|
+
output !== undefined ? JSON.stringify(output) : '__NONE__';
|
|
611
|
+
await this.client.eval(
|
|
612
|
+
COMPLETE_JOB_SCRIPT,
|
|
613
|
+
1,
|
|
614
|
+
this.prefix,
|
|
615
|
+
jobId,
|
|
616
|
+
now,
|
|
617
|
+
outputArg,
|
|
618
|
+
);
|
|
475
619
|
await this.recordJobEvent(jobId, JobEventType.Completed);
|
|
476
620
|
log(`Completed job ${jobId}`);
|
|
477
621
|
}
|
|
@@ -535,6 +679,24 @@ export class RedisBackend implements QueueBackend {
|
|
|
535
679
|
}
|
|
536
680
|
}
|
|
537
681
|
|
|
682
|
+
// ── Output ────────────────────────────────────────────────────────────
|
|
683
|
+
|
|
684
|
+
async updateOutput(jobId: number, output: unknown): Promise<void> {
|
|
685
|
+
try {
|
|
686
|
+
const now = this.nowMs();
|
|
687
|
+
await this.client.hset(
|
|
688
|
+
`${this.prefix}job:${jobId}`,
|
|
689
|
+
'output',
|
|
690
|
+
JSON.stringify(output),
|
|
691
|
+
'updatedAt',
|
|
692
|
+
now.toString(),
|
|
693
|
+
);
|
|
694
|
+
log(`Updated output for job ${jobId}`);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
log(`Error updating output for job ${jobId}: ${error}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
538
700
|
// ── Job management ────────────────────────────────────────────────────
|
|
539
701
|
|
|
540
702
|
async retryJob(jobId: number): Promise<void> {
|
|
@@ -657,6 +819,31 @@ export class RedisBackend implements QueueBackend {
|
|
|
657
819
|
}
|
|
658
820
|
metadata.tags = updates.tags;
|
|
659
821
|
}
|
|
822
|
+
if (updates.retryDelay !== undefined) {
|
|
823
|
+
fields.push(
|
|
824
|
+
'retryDelay',
|
|
825
|
+
updates.retryDelay !== null ? updates.retryDelay.toString() : 'null',
|
|
826
|
+
);
|
|
827
|
+
metadata.retryDelay = updates.retryDelay;
|
|
828
|
+
}
|
|
829
|
+
if (updates.retryBackoff !== undefined) {
|
|
830
|
+
fields.push(
|
|
831
|
+
'retryBackoff',
|
|
832
|
+
updates.retryBackoff !== null
|
|
833
|
+
? updates.retryBackoff.toString()
|
|
834
|
+
: 'null',
|
|
835
|
+
);
|
|
836
|
+
metadata.retryBackoff = updates.retryBackoff;
|
|
837
|
+
}
|
|
838
|
+
if (updates.retryDelayMax !== undefined) {
|
|
839
|
+
fields.push(
|
|
840
|
+
'retryDelayMax',
|
|
841
|
+
updates.retryDelayMax !== null
|
|
842
|
+
? updates.retryDelayMax.toString()
|
|
843
|
+
: 'null',
|
|
844
|
+
);
|
|
845
|
+
metadata.retryDelayMax = updates.retryDelayMax;
|
|
846
|
+
}
|
|
660
847
|
|
|
661
848
|
if (fields.length === 0) {
|
|
662
849
|
log(`No fields to update for job ${jobId}`);
|
|
@@ -1236,6 +1423,18 @@ export class RedisBackend implements QueueBackend {
|
|
|
1236
1423
|
now.toString(),
|
|
1237
1424
|
'updatedAt',
|
|
1238
1425
|
now.toString(),
|
|
1426
|
+
'retryDelay',
|
|
1427
|
+
input.retryDelay !== null && input.retryDelay !== undefined
|
|
1428
|
+
? input.retryDelay.toString()
|
|
1429
|
+
: 'null',
|
|
1430
|
+
'retryBackoff',
|
|
1431
|
+
input.retryBackoff !== null && input.retryBackoff !== undefined
|
|
1432
|
+
? input.retryBackoff.toString()
|
|
1433
|
+
: 'null',
|
|
1434
|
+
'retryDelayMax',
|
|
1435
|
+
input.retryDelayMax !== null && input.retryDelayMax !== undefined
|
|
1436
|
+
? input.retryDelayMax.toString()
|
|
1437
|
+
: 'null',
|
|
1239
1438
|
];
|
|
1240
1439
|
|
|
1241
1440
|
await (this.client as any).hmset(key, ...fields);
|
|
@@ -1417,6 +1616,28 @@ export class RedisBackend implements QueueBackend {
|
|
|
1417
1616
|
if (updates.allowOverlap !== undefined) {
|
|
1418
1617
|
fields.push('allowOverlap', updates.allowOverlap ? 'true' : 'false');
|
|
1419
1618
|
}
|
|
1619
|
+
if (updates.retryDelay !== undefined) {
|
|
1620
|
+
fields.push(
|
|
1621
|
+
'retryDelay',
|
|
1622
|
+
updates.retryDelay !== null ? updates.retryDelay.toString() : 'null',
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
if (updates.retryBackoff !== undefined) {
|
|
1626
|
+
fields.push(
|
|
1627
|
+
'retryBackoff',
|
|
1628
|
+
updates.retryBackoff !== null
|
|
1629
|
+
? updates.retryBackoff.toString()
|
|
1630
|
+
: 'null',
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
if (updates.retryDelayMax !== undefined) {
|
|
1634
|
+
fields.push(
|
|
1635
|
+
'retryDelayMax',
|
|
1636
|
+
updates.retryDelayMax !== null
|
|
1637
|
+
? updates.retryDelayMax.toString()
|
|
1638
|
+
: 'null',
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1420
1641
|
if (nextRunAt !== undefined) {
|
|
1421
1642
|
const val = nextRunAt !== null ? nextRunAt.getTime().toString() : 'null';
|
|
1422
1643
|
fields.push('nextRunAt', val);
|
|
@@ -1557,6 +1778,14 @@ export class RedisBackend implements QueueBackend {
|
|
|
1557
1778
|
nextRunAt: dateOrNull(h.nextRunAt),
|
|
1558
1779
|
createdAt: new Date(Number(h.createdAt)),
|
|
1559
1780
|
updatedAt: new Date(Number(h.updatedAt)),
|
|
1781
|
+
retryDelay: numOrNull(h.retryDelay),
|
|
1782
|
+
retryBackoff:
|
|
1783
|
+
h.retryBackoff === 'true'
|
|
1784
|
+
? true
|
|
1785
|
+
: h.retryBackoff === 'false'
|
|
1786
|
+
? false
|
|
1787
|
+
: null,
|
|
1788
|
+
retryDelayMax: numOrNull(h.retryDelayMax),
|
|
1560
1789
|
};
|
|
1561
1790
|
}
|
|
1562
1791
|
|
package/src/cli.test.ts
CHANGED
|
@@ -23,6 +23,10 @@ function makeDeps() {
|
|
|
23
23
|
spawnSyncImpl: vi.fn(() => makeSpawnSyncReturns(0)),
|
|
24
24
|
migrationsDir: '/migrations',
|
|
25
25
|
runInitImpl: vi.fn(),
|
|
26
|
+
runInstallSkillsImpl: vi.fn(),
|
|
27
|
+
runInstallRulesImpl: vi.fn(async () => {}),
|
|
28
|
+
runInstallMcpImpl: vi.fn(async () => {}),
|
|
29
|
+
startMcpServerImpl: vi.fn(async () => ({}) as any),
|
|
26
30
|
} satisfies CliDeps;
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -138,4 +142,65 @@ describe('runCli', () => {
|
|
|
138
142
|
runCli(['node', 'cli.js', 'migrate'], deps);
|
|
139
143
|
expect(deps.exit).toHaveBeenCalledWith(1);
|
|
140
144
|
});
|
|
145
|
+
|
|
146
|
+
it('routes install-skills command to runInstallSkillsImpl', () => {
|
|
147
|
+
// Act
|
|
148
|
+
runCli(['node', 'cli.js', 'install-skills'], deps);
|
|
149
|
+
|
|
150
|
+
// Assert
|
|
151
|
+
expect(deps.runInstallSkillsImpl).toHaveBeenCalledWith(
|
|
152
|
+
expect.objectContaining({
|
|
153
|
+
log: deps.log,
|
|
154
|
+
error: deps.error,
|
|
155
|
+
exit: deps.exit,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('routes install-rules command to runInstallRulesImpl', () => {
|
|
161
|
+
// Act
|
|
162
|
+
runCli(['node', 'cli.js', 'install-rules'], deps);
|
|
163
|
+
|
|
164
|
+
// Assert
|
|
165
|
+
expect(deps.runInstallRulesImpl).toHaveBeenCalledWith(
|
|
166
|
+
expect.objectContaining({
|
|
167
|
+
log: deps.log,
|
|
168
|
+
error: deps.error,
|
|
169
|
+
exit: deps.exit,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('routes install-mcp command to runInstallMcpImpl', () => {
|
|
175
|
+
// Act
|
|
176
|
+
runCli(['node', 'cli.js', 'install-mcp'], deps);
|
|
177
|
+
|
|
178
|
+
// Assert
|
|
179
|
+
expect(deps.runInstallMcpImpl).toHaveBeenCalledWith(
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
log: deps.log,
|
|
182
|
+
error: deps.error,
|
|
183
|
+
exit: deps.exit,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('routes mcp command to startMcpServerImpl', () => {
|
|
189
|
+
// Act
|
|
190
|
+
runCli(['node', 'cli.js', 'mcp'], deps);
|
|
191
|
+
|
|
192
|
+
// Assert
|
|
193
|
+
expect(deps.startMcpServerImpl).toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('shows new commands in usage output', () => {
|
|
197
|
+
// Act
|
|
198
|
+
runCli(['node', 'cli.js'], deps);
|
|
199
|
+
|
|
200
|
+
// Assert
|
|
201
|
+
expect(deps.log).toHaveBeenCalledWith(' dataqueue-cli install-skills');
|
|
202
|
+
expect(deps.log).toHaveBeenCalledWith(' dataqueue-cli install-rules');
|
|
203
|
+
expect(deps.log).toHaveBeenCalledWith(' dataqueue-cli install-mcp');
|
|
204
|
+
expect(deps.log).toHaveBeenCalledWith(' dataqueue-cli mcp');
|
|
205
|
+
});
|
|
141
206
|
});
|
package/src/cli.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { spawnSync, SpawnSyncReturns } from 'child_process';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { InitDeps, runInit } from './init-command.js';
|
|
6
|
+
import {
|
|
7
|
+
runInstallSkills,
|
|
8
|
+
InstallSkillsDeps,
|
|
9
|
+
} from './install-skills-command.js';
|
|
10
|
+
import { runInstallRules, InstallRulesDeps } from './install-rules-command.js';
|
|
11
|
+
import { runInstallMcp, InstallMcpDeps } from './install-mcp-command.js';
|
|
12
|
+
import { startMcpServer } from './mcp-server.js';
|
|
6
13
|
|
|
7
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -15,6 +22,13 @@ export interface CliDeps {
|
|
|
15
22
|
migrationsDir?: string;
|
|
16
23
|
initDeps?: InitDeps;
|
|
17
24
|
runInitImpl?: (deps?: InitDeps) => void;
|
|
25
|
+
installSkillsDeps?: InstallSkillsDeps;
|
|
26
|
+
runInstallSkillsImpl?: (deps?: InstallSkillsDeps) => void;
|
|
27
|
+
installRulesDeps?: InstallRulesDeps;
|
|
28
|
+
runInstallRulesImpl?: (deps?: InstallRulesDeps) => Promise<void>;
|
|
29
|
+
installMcpDeps?: InstallMcpDeps;
|
|
30
|
+
runInstallMcpImpl?: (deps?: InstallMcpDeps) => Promise<void>;
|
|
31
|
+
startMcpServerImpl?: typeof startMcpServer;
|
|
18
32
|
}
|
|
19
33
|
|
|
20
34
|
export function runCli(
|
|
@@ -27,19 +41,27 @@ export function runCli(
|
|
|
27
41
|
migrationsDir = path.join(__dirname, '../migrations'),
|
|
28
42
|
initDeps,
|
|
29
43
|
runInitImpl = runInit,
|
|
44
|
+
installSkillsDeps,
|
|
45
|
+
runInstallSkillsImpl = runInstallSkills,
|
|
46
|
+
installRulesDeps,
|
|
47
|
+
runInstallRulesImpl = runInstallRules,
|
|
48
|
+
installMcpDeps,
|
|
49
|
+
runInstallMcpImpl = runInstallMcp,
|
|
50
|
+
startMcpServerImpl = startMcpServer,
|
|
30
51
|
}: CliDeps = {},
|
|
31
52
|
): void {
|
|
32
53
|
const [, , command, ...restArgs] = argv;
|
|
33
54
|
|
|
34
|
-
/**
|
|
35
|
-
* Prints CLI usage and exits with non-zero code.
|
|
36
|
-
*/
|
|
37
55
|
function printUsage() {
|
|
38
56
|
log('Usage:');
|
|
39
57
|
log(
|
|
40
58
|
' dataqueue-cli migrate [--envPath <path>] [-s <schema> | --schema <schema>]',
|
|
41
59
|
);
|
|
42
60
|
log(' dataqueue-cli init');
|
|
61
|
+
log(' dataqueue-cli install-skills');
|
|
62
|
+
log(' dataqueue-cli install-rules');
|
|
63
|
+
log(' dataqueue-cli install-mcp');
|
|
64
|
+
log(' dataqueue-cli mcp');
|
|
43
65
|
log('');
|
|
44
66
|
log('Options for migrate:');
|
|
45
67
|
log(
|
|
@@ -49,24 +71,13 @@ export function runCli(
|
|
|
49
71
|
' -s, --schema <schema> Set the schema to use (passed to node-pg-migrate)',
|
|
50
72
|
);
|
|
51
73
|
log('');
|
|
52
|
-
log('
|
|
74
|
+
log('AI tooling commands:');
|
|
75
|
+
log(' install-skills Install DataQueue skill files for AI assistants');
|
|
76
|
+
log(' install-rules Install DataQueue agent rules for AI clients');
|
|
53
77
|
log(
|
|
54
|
-
' -
|
|
55
|
-
);
|
|
56
|
-
log(
|
|
57
|
-
' - For managed Postgres (e.g., DigitalOcean) with SSL, set PGSSLMODE=require and PGSSLROOTCERT to your CA .crt file.',
|
|
58
|
-
);
|
|
59
|
-
log(
|
|
60
|
-
' Example: PGSSLMODE=require NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.crt PG_DATAQUEUE_DATABASE=... npx dataqueue-cli migrate',
|
|
61
|
-
);
|
|
62
|
-
log('');
|
|
63
|
-
log('Notes for init:');
|
|
64
|
-
log(
|
|
65
|
-
' - Supports both Next.js App Router and Pages Router (prefers App Router if both exist).',
|
|
66
|
-
);
|
|
67
|
-
log(
|
|
68
|
-
' - Scaffolds endpoint, cron.sh, queue placeholder, and package.json entries.',
|
|
78
|
+
' install-mcp Configure the DataQueue MCP server for AI clients',
|
|
69
79
|
);
|
|
80
|
+
log(' mcp Start the DataQueue MCP server (stdio)');
|
|
70
81
|
exit(1);
|
|
71
82
|
}
|
|
72
83
|
|
|
@@ -115,6 +126,32 @@ export function runCli(
|
|
|
115
126
|
exit,
|
|
116
127
|
...initDeps,
|
|
117
128
|
});
|
|
129
|
+
} else if (command === 'install-skills') {
|
|
130
|
+
runInstallSkillsImpl({
|
|
131
|
+
log,
|
|
132
|
+
error,
|
|
133
|
+
exit,
|
|
134
|
+
...installSkillsDeps,
|
|
135
|
+
});
|
|
136
|
+
} else if (command === 'install-rules') {
|
|
137
|
+
runInstallRulesImpl({
|
|
138
|
+
log,
|
|
139
|
+
error,
|
|
140
|
+
exit,
|
|
141
|
+
...installRulesDeps,
|
|
142
|
+
});
|
|
143
|
+
} else if (command === 'install-mcp') {
|
|
144
|
+
runInstallMcpImpl({
|
|
145
|
+
log,
|
|
146
|
+
error,
|
|
147
|
+
exit,
|
|
148
|
+
...installMcpDeps,
|
|
149
|
+
});
|
|
150
|
+
} else if (command === 'mcp') {
|
|
151
|
+
startMcpServerImpl().catch((err) => {
|
|
152
|
+
error('Failed to start MCP server:', err);
|
|
153
|
+
exit(1);
|
|
154
|
+
});
|
|
118
155
|
} else {
|
|
119
156
|
printUsage();
|
|
120
157
|
}
|
package/src/db-util.ts
CHANGED
|
@@ -27,7 +27,7 @@ function loadPemOrFile(value?: string): string | undefined {
|
|
|
27
27
|
* }
|
|
28
28
|
*/
|
|
29
29
|
export const createPool = (
|
|
30
|
-
config: PostgresJobQueueConfig['databaseConfig']
|
|
30
|
+
config: NonNullable<PostgresJobQueueConfig['databaseConfig']>,
|
|
31
31
|
): Pool => {
|
|
32
32
|
let searchPath: string | undefined;
|
|
33
33
|
let ssl: any = undefined;
|