@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.
package/dist/cli.cjs CHANGED
@@ -1,30 +1,1135 @@
1
1
  'use strict';
2
2
 
3
3
  var child_process = require('child_process');
4
- var path = require('path');
4
+ var path3 = require('path');
5
5
  var url = require('url');
6
+ var fs = require('fs');
7
+ var readline = require('readline');
8
+ var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
9
+ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
10
+ var zod = require('zod');
6
11
 
7
12
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
13
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
14
 
10
- var path__default = /*#__PURE__*/_interopDefault(path);
15
+ var path3__default = /*#__PURE__*/_interopDefault(path3);
16
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
17
+ var readline__default = /*#__PURE__*/_interopDefault(readline);
11
18
 
12
19
  // src/cli.ts
20
+ var DEPENDENCIES_TO_ADD = [
21
+ "@nicnocquee/dataqueue",
22
+ "@nicnocquee/dataqueue-dashboard",
23
+ "@nicnocquee/dataqueue-react"
24
+ ];
25
+ var DEV_DEPENDENCIES_TO_ADD = [
26
+ "dotenv-cli",
27
+ "ts-node",
28
+ "node-pg-migrate"
29
+ ];
30
+ var SCRIPTS_TO_ADD = {
31
+ cron: "bash cron.sh",
32
+ "migrate-dataqueue": "dotenv -e .env.local -- dataqueue-cli migrate"
33
+ };
34
+ var APP_ROUTER_ROUTE_TEMPLATE = `/**
35
+ * This end point is used to manage the job queue.
36
+ * It supports the following tasks:
37
+ * - reclaim: Reclaim stuck jobs
38
+ * - cleanup: Cleanup old jobs
39
+ * - process: Process jobs
40
+ *
41
+ * Example usage with default values (reclaim stuck jobs for 10 minutes, cleanup old jobs for 30 days, and process jobs with batch size 3, concurrency 2, and verbose true):
42
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/reclaim -H "Authorization: Bearer $CRON_SECRET"
43
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/cleanup -H "Authorization: Bearer $CRON_SECRET"
44
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/process -H "Authorization: Bearer $CRON_SECRET"
45
+ *
46
+ * Example usage with custom values:
47
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/reclaim -H "Authorization: Bearer $CRON_SECRET" -d '{"maxProcessingTimeMinutes": 15}' -H "Content-Type: application/json"
48
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/cleanup -H "Authorization: Bearer $CRON_SECRET" -d '{"daysToKeep": 15}' -H "Content-Type: application/json"
49
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/process -H "Authorization: Bearer $CRON_SECRET" -d '{"batchSize": 5, "concurrency": 3, "verbose": false, "workerId": "custom-worker-id"}' -H "Content-Type: application/json"
50
+ *
51
+ * During development, you can run the following script to run the cron jobs continuously in the background:
52
+ * pnpm cron
53
+ */
54
+ import { getJobQueue, jobHandlers } from '@/lib/dataqueue/queue';
55
+ import { NextResponse } from 'next/server';
56
+
57
+ export async function POST(
58
+ request: Request,
59
+ { params }: { params: Promise<{ task: string[] }> },
60
+ ) {
61
+ const { task } = await params;
62
+ const authHeader = request.headers.get('authorization');
63
+ if (authHeader !== \`Bearer \${process.env.CRON_SECRET}\`) {
64
+ return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
65
+ }
66
+
67
+ if (!task || task.length === 0) {
68
+ return NextResponse.json({ message: 'Task is required' }, { status: 400 });
69
+ }
70
+
71
+ const supportedTasks = ['reclaim', 'cleanup', 'process'];
72
+ const theTask = task[0];
73
+ if (!supportedTasks.includes(theTask)) {
74
+ return NextResponse.json(
75
+ { message: 'Task not supported' },
76
+ { status: 400 },
77
+ );
78
+ }
79
+
80
+ try {
81
+ const jobQueue = getJobQueue();
82
+
83
+ if (theTask === 'reclaim') {
84
+ let maxProcessingTimeMinutes = 10;
85
+ try {
86
+ const body = await request.json();
87
+ maxProcessingTimeMinutes = body.maxProcessingTimeMinutes || 10;
88
+ } catch {
89
+ // ignore parsing error and use default value
90
+ }
91
+ const reclaimed = await jobQueue.reclaimStuckJobs(
92
+ maxProcessingTimeMinutes,
93
+ );
94
+ console.log(\`Reclaimed \${reclaimed} stuck jobs\`);
95
+ return NextResponse.json({
96
+ message: \`Stuck jobs reclaimed: \${reclaimed} with maxProcessingTimeMinutes: \${maxProcessingTimeMinutes}\`,
97
+ reclaimed,
98
+ });
99
+ }
100
+
101
+ if (theTask === 'cleanup') {
102
+ let daysToKeep = 30;
103
+ try {
104
+ const body = await request.json();
105
+ daysToKeep = body.daysToKeep || 30;
106
+ } catch {
107
+ // ignore parsing error and use default value
108
+ }
109
+ const deleted = await jobQueue.cleanupOldJobs(daysToKeep);
110
+ console.log(\`Deleted \${deleted} old jobs\`);
111
+ return NextResponse.json({
112
+ message: \`Old jobs cleaned up: \${deleted} with daysToKeep: \${daysToKeep}\`,
113
+ deleted,
114
+ });
115
+ }
116
+
117
+ if (theTask === 'process') {
118
+ let batchSize = 3;
119
+ let concurrency = 2;
120
+ let verbose = true;
121
+ let workerId = \`manage-\${theTask}-\${Date.now()}\`;
122
+ try {
123
+ const body = await request.json();
124
+ batchSize = body.batchSize || 3;
125
+ concurrency = body.concurrency || 2;
126
+ verbose = body.verbose || true;
127
+ workerId = body.workerId || \`manage-\${theTask}-\${Date.now()}\`;
128
+ } catch {
129
+ // ignore parsing error and use default value
130
+ }
131
+ const processor = jobQueue.createProcessor(jobHandlers, {
132
+ workerId,
133
+ batchSize,
134
+ concurrency,
135
+ verbose,
136
+ });
137
+ const processed = await processor.start();
138
+
139
+ return NextResponse.json({
140
+ message: \`Jobs processed: \${processed} with workerId: \${workerId}, batchSize: \${batchSize}, concurrency: \${concurrency}, and verbose: \${verbose}\`,
141
+ processed,
142
+ });
143
+ }
144
+
145
+ return NextResponse.json(
146
+ { message: 'Task not supported' },
147
+ { status: 400 },
148
+ );
149
+ } catch (error) {
150
+ console.error('Error processing jobs:', error);
151
+ return NextResponse.json(
152
+ { message: 'Failed to process jobs' },
153
+ { status: 500 },
154
+ );
155
+ }
156
+ }
157
+ `;
158
+ var PAGES_ROUTER_ROUTE_TEMPLATE = `/**
159
+ * This end point is used to manage the job queue.
160
+ * It supports the following tasks:
161
+ * - reclaim: Reclaim stuck jobs
162
+ * - cleanup: Cleanup old jobs
163
+ * - process: Process jobs
164
+ *
165
+ * Example usage with default values (reclaim stuck jobs for 10 minutes, cleanup old jobs for 30 days, and process jobs with batch size 3, concurrency 2, and verbose true):
166
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/reclaim -H "Authorization: Bearer $CRON_SECRET"
167
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/cleanup -H "Authorization: Bearer $CRON_SECRET"
168
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/process -H "Authorization: Bearer $CRON_SECRET"
169
+ *
170
+ * Example usage with custom values:
171
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/reclaim -H "Authorization: Bearer $CRON_SECRET" -d '{"maxProcessingTimeMinutes": 15}' -H "Content-Type: application/json"
172
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/cleanup -H "Authorization: Bearer $CRON_SECRET" -d '{"daysToKeep": 15}' -H "Content-Type: application/json"
173
+ * curl -X POST http://localhost:3000/api/dataqueue/manage/process -H "Authorization: Bearer $CRON_SECRET" -d '{"batchSize": 5, "concurrency": 3, "verbose": false, "workerId": "custom-worker-id"}' -H "Content-Type: application/json"
174
+ *
175
+ * During development, you can run the following script to run the cron jobs continuously in the background:
176
+ * pnpm cron
177
+ */
178
+ import type { NextApiRequest, NextApiResponse } from 'next';
179
+ import { getJobQueue, jobHandlers } from '@/lib/dataqueue/queue';
180
+
181
+ type ResponseBody = {
182
+ message: string;
183
+ reclaimed?: number;
184
+ deleted?: number;
185
+ processed?: number;
186
+ };
187
+
188
+ export default async function handler(
189
+ req: NextApiRequest,
190
+ res: NextApiResponse<ResponseBody>,
191
+ ) {
192
+ if (req.method !== 'POST') {
193
+ res.setHeader('Allow', 'POST');
194
+ return res.status(405).json({ message: 'Method not allowed' });
195
+ }
196
+
197
+ const authHeader = req.headers.authorization;
198
+ if (authHeader !== \`Bearer \${process.env.CRON_SECRET}\`) {
199
+ return res.status(401).json({ message: 'Unauthorized' });
200
+ }
201
+
202
+ const task = req.query.task;
203
+ const taskArray = Array.isArray(task) ? task : task ? [task] : [];
204
+ if (!taskArray.length) {
205
+ return res.status(400).json({ message: 'Task is required' });
206
+ }
207
+
208
+ const supportedTasks = ['reclaim', 'cleanup', 'process'];
209
+ const theTask = taskArray[0];
210
+ if (!supportedTasks.includes(theTask)) {
211
+ return res.status(400).json({ message: 'Task not supported' });
212
+ }
213
+
214
+ try {
215
+ const jobQueue = getJobQueue();
216
+ const body = typeof req.body === 'object' && req.body ? req.body : {};
217
+
218
+ if (theTask === 'reclaim') {
219
+ const maxProcessingTimeMinutes = body.maxProcessingTimeMinutes || 10;
220
+ const reclaimed = await jobQueue.reclaimStuckJobs(maxProcessingTimeMinutes);
221
+ console.log(\`Reclaimed \${reclaimed} stuck jobs\`);
222
+ return res.status(200).json({
223
+ message: \`Stuck jobs reclaimed: \${reclaimed} with maxProcessingTimeMinutes: \${maxProcessingTimeMinutes}\`,
224
+ reclaimed,
225
+ });
226
+ }
227
+
228
+ if (theTask === 'cleanup') {
229
+ const daysToKeep = body.daysToKeep || 30;
230
+ const deleted = await jobQueue.cleanupOldJobs(daysToKeep);
231
+ console.log(\`Deleted \${deleted} old jobs\`);
232
+ return res.status(200).json({
233
+ message: \`Old jobs cleaned up: \${deleted} with daysToKeep: \${daysToKeep}\`,
234
+ deleted,
235
+ });
236
+ }
237
+
238
+ const batchSize = body.batchSize || 3;
239
+ const concurrency = body.concurrency || 2;
240
+ const verbose = body.verbose || true;
241
+ const workerId = body.workerId || \`manage-\${theTask}-\${Date.now()}\`;
242
+ const processor = jobQueue.createProcessor(jobHandlers, {
243
+ workerId,
244
+ batchSize,
245
+ concurrency,
246
+ verbose,
247
+ });
248
+ const processed = await processor.start();
249
+
250
+ return res.status(200).json({
251
+ message: \`Jobs processed: \${processed} with workerId: \${workerId}, batchSize: \${batchSize}, concurrency: \${concurrency}, and verbose: \${verbose}\`,
252
+ processed,
253
+ });
254
+ } catch (error) {
255
+ console.error('Error processing jobs:', error);
256
+ return res.status(500).json({ message: 'Failed to process jobs' });
257
+ }
258
+ }
259
+ `;
260
+ var CRON_SH_TEMPLATE = `#!/bin/bash
261
+
262
+ # This script is used to run the cron jobs for the demo app during development.
263
+ # Run it with \`pnpm cron\` from the apps/demo directory.
264
+
265
+ set -a
266
+ source "$(dirname "$0")/.env.local"
267
+ set +a
268
+
269
+ if [ -z "$CRON_SECRET" ]; then
270
+ echo "Error: CRON_SECRET environment variable is not set in .env.local"
271
+ exit 1
272
+ fi
273
+
274
+ cleanup() {
275
+ kill 0
276
+ wait
277
+ }
278
+ trap cleanup SIGINT SIGTERM
279
+
280
+ while true; do
281
+ echo "Processing jobs..."
282
+ curl http://localhost:3000/api/dataqueue/manage/process -X POST -H "Authorization: Bearer $CRON_SECRET"
283
+ echo ""
284
+ sleep 10 # Process jobs every 10 seconds
285
+ done &
286
+
287
+ while true; do
288
+ echo "Reclaiming stuck jobs..."
289
+ curl http://localhost:3000/api/dataqueue/manage/reclaim -X POST -H "Authorization: Bearer $CRON_SECRET"
290
+ echo ""
291
+ sleep 20 # Reclaim stuck jobs every 20 seconds
292
+ done &
293
+
294
+ while true; do
295
+ echo "Cleaning up old jobs..."
296
+ curl http://localhost:3000/api/dataqueue/manage/cleanup -X POST -H "Authorization: Bearer $CRON_SECRET"
297
+ echo ""
298
+ sleep 30 # Cleanup old jobs every 30 seconds
299
+ done &
300
+
301
+ wait
302
+ `;
303
+ var QUEUE_TEMPLATE = `import { initJobQueue, JobHandlers } from '@nicnocquee/dataqueue';
304
+
305
+ export type JobPayloadMap = {
306
+ send_email: {
307
+ to: string;
308
+ subject: string;
309
+ body: string;
310
+ };
311
+ };
312
+
313
+ let jobQueue: ReturnType<typeof initJobQueue<JobPayloadMap>> | null = null;
314
+
315
+ export const getJobQueue = () => {
316
+ if (!jobQueue) {
317
+ jobQueue = initJobQueue<JobPayloadMap>({
318
+ databaseConfig: {
319
+ connectionString: process.env.PG_DATAQUEUE_DATABASE,
320
+ },
321
+ verbose: process.env.NODE_ENV === 'development',
322
+ });
323
+ }
324
+ return jobQueue;
325
+ };
326
+
327
+ export const jobHandlers: JobHandlers<JobPayloadMap> = {
328
+ send_email: async (payload) => {
329
+ const { to, subject, body } = payload;
330
+ console.log('send_email placeholder:', { to, subject, body });
331
+ },
332
+ };
333
+ `;
334
+ function runInit({
335
+ log = console.log,
336
+ error = console.error,
337
+ exit = (code) => process.exit(code),
338
+ cwd = process.cwd(),
339
+ readFileSyncImpl = fs.readFileSync,
340
+ writeFileSyncImpl = fs.writeFileSync,
341
+ existsSyncImpl = fs.existsSync,
342
+ mkdirSyncImpl = fs.mkdirSync,
343
+ chmodSyncImpl = fs.chmodSync
344
+ } = {}) {
345
+ try {
346
+ log(`dataqueue: Initializing in ${cwd}...`);
347
+ log("");
348
+ const details = detectNextJsAndRouter({
349
+ cwd,
350
+ existsSyncImpl,
351
+ readFileSyncImpl
352
+ });
353
+ createScaffoldFiles({
354
+ details,
355
+ log,
356
+ existsSyncImpl,
357
+ mkdirSyncImpl,
358
+ writeFileSyncImpl,
359
+ chmodSyncImpl
360
+ });
361
+ updatePackageJson({
362
+ details,
363
+ log,
364
+ writeFileSyncImpl
365
+ });
366
+ log("");
367
+ log(
368
+ "Done! Run your package manager's install command to install new dependencies."
369
+ );
370
+ exit(0);
371
+ } catch (cause) {
372
+ const message = cause instanceof Error ? cause.message : String(cause);
373
+ error(`dataqueue: ${message}`);
374
+ exit(1);
375
+ }
376
+ }
377
+ function detectNextJsAndRouter({
378
+ cwd,
379
+ existsSyncImpl,
380
+ readFileSyncImpl
381
+ }) {
382
+ const packageJsonPath = path3__default.default.join(cwd, "package.json");
383
+ if (!existsSyncImpl(packageJsonPath)) {
384
+ throw new Error("package.json not found in current directory.");
385
+ }
386
+ const packageJson = parsePackageJson(
387
+ readFileSyncImpl(packageJsonPath, "utf8"),
388
+ packageJsonPath
389
+ );
390
+ if (!isNextJsProject(packageJson)) {
391
+ throw new Error(
392
+ "Not a Next.js project. Could not find 'next' in package.json dependencies."
393
+ );
394
+ }
395
+ const srcDir = path3__default.default.join(cwd, "src");
396
+ const srcRoot = existsSyncImpl(srcDir) ? "src" : ".";
397
+ const appDir = path3__default.default.join(cwd, srcRoot, "app");
398
+ const pagesDir = path3__default.default.join(cwd, srcRoot, "pages");
399
+ const hasAppDir = existsSyncImpl(appDir);
400
+ const hasPagesDir = existsSyncImpl(pagesDir);
401
+ if (!hasAppDir && !hasPagesDir) {
402
+ throw new Error(
403
+ "Could not detect Next.js router. Expected either app/ or pages/ directory."
404
+ );
405
+ }
406
+ const router = hasAppDir ? "app" : "pages";
407
+ return { cwd, packageJsonPath, packageJson, srcRoot, router };
408
+ }
409
+ function updatePackageJson({
410
+ details,
411
+ log,
412
+ writeFileSyncImpl
413
+ }) {
414
+ const packageJson = details.packageJson;
415
+ const dependencies = ensureStringMapSection(packageJson, "dependencies");
416
+ const devDependencies = ensureStringMapSection(
417
+ packageJson,
418
+ "devDependencies"
419
+ );
420
+ const scripts = ensureStringMapSection(packageJson, "scripts");
421
+ for (const dependency of DEPENDENCIES_TO_ADD) {
422
+ if (dependencies[dependency]) {
423
+ log(` [skipped] dependency ${dependency} (already exists)`);
424
+ continue;
425
+ }
426
+ dependencies[dependency] = "latest";
427
+ log(` [added] dependency ${dependency}`);
428
+ }
429
+ for (const devDependency of DEV_DEPENDENCIES_TO_ADD) {
430
+ if (devDependencies[devDependency]) {
431
+ log(` [skipped] devDependency ${devDependency} (already exists)`);
432
+ continue;
433
+ }
434
+ devDependencies[devDependency] = "latest";
435
+ log(` [added] devDependency ${devDependency}`);
436
+ }
437
+ for (const [scriptName, scriptValue] of Object.entries(SCRIPTS_TO_ADD)) {
438
+ if (scripts[scriptName]) {
439
+ log(` [skipped] script "${scriptName}" (already exists)`);
440
+ continue;
441
+ }
442
+ scripts[scriptName] = scriptValue;
443
+ log(` [added] script "${scriptName}"`);
444
+ }
445
+ writeFileSyncImpl(
446
+ details.packageJsonPath,
447
+ `${JSON.stringify(packageJson, null, 2)}
448
+ `
449
+ );
450
+ }
451
+ function createScaffoldFiles({
452
+ details,
453
+ log,
454
+ existsSyncImpl,
455
+ mkdirSyncImpl,
456
+ writeFileSyncImpl,
457
+ chmodSyncImpl
458
+ }) {
459
+ const appRoutePath = path3__default.default.join(
460
+ details.cwd,
461
+ details.srcRoot,
462
+ "app",
463
+ "api",
464
+ "dataqueue",
465
+ "manage",
466
+ "[[...task]]",
467
+ "route.ts"
468
+ );
469
+ const pagesRoutePath = path3__default.default.join(
470
+ details.cwd,
471
+ details.srcRoot,
472
+ "pages",
473
+ "api",
474
+ "dataqueue",
475
+ "manage",
476
+ "[[...task]].ts"
477
+ );
478
+ const queuePath = path3__default.default.join(
479
+ details.cwd,
480
+ details.srcRoot,
481
+ "lib",
482
+ "dataqueue",
483
+ "queue.ts"
484
+ );
485
+ const cronPath = path3__default.default.join(details.cwd, "cron.sh");
486
+ if (details.router === "app") {
487
+ createFileIfMissing({
488
+ absolutePath: appRoutePath,
489
+ content: APP_ROUTER_ROUTE_TEMPLATE,
490
+ existsSyncImpl,
491
+ mkdirSyncImpl,
492
+ writeFileSyncImpl,
493
+ log,
494
+ logPath: toRelativePath(details.cwd, appRoutePath)
495
+ });
496
+ log(
497
+ " [skipped] pages/api/dataqueue/manage/[[...task]].ts (router not selected)"
498
+ );
499
+ } else {
500
+ log(
501
+ " [skipped] app/api/dataqueue/manage/[[...task]]/route.ts (router not selected)"
502
+ );
503
+ createFileIfMissing({
504
+ absolutePath: pagesRoutePath,
505
+ content: PAGES_ROUTER_ROUTE_TEMPLATE,
506
+ existsSyncImpl,
507
+ mkdirSyncImpl,
508
+ writeFileSyncImpl,
509
+ log,
510
+ logPath: toRelativePath(details.cwd, pagesRoutePath)
511
+ });
512
+ }
513
+ createFileIfMissing({
514
+ absolutePath: cronPath,
515
+ content: CRON_SH_TEMPLATE,
516
+ existsSyncImpl,
517
+ mkdirSyncImpl,
518
+ writeFileSyncImpl,
519
+ log,
520
+ logPath: "cron.sh"
521
+ });
522
+ if (existsSyncImpl(cronPath)) {
523
+ chmodSyncImpl(cronPath, 493);
524
+ }
525
+ createFileIfMissing({
526
+ absolutePath: queuePath,
527
+ content: QUEUE_TEMPLATE,
528
+ existsSyncImpl,
529
+ mkdirSyncImpl,
530
+ writeFileSyncImpl,
531
+ log,
532
+ logPath: toRelativePath(details.cwd, queuePath)
533
+ });
534
+ }
535
+ function createFileIfMissing({
536
+ absolutePath,
537
+ content,
538
+ existsSyncImpl,
539
+ mkdirSyncImpl,
540
+ writeFileSyncImpl,
541
+ log,
542
+ logPath
543
+ }) {
544
+ if (existsSyncImpl(absolutePath)) {
545
+ log(` [skipped] ${logPath} (already exists)`);
546
+ return;
547
+ }
548
+ mkdirSyncImpl(path3__default.default.dirname(absolutePath), { recursive: true });
549
+ writeFileSyncImpl(absolutePath, content);
550
+ log(` [created] ${logPath}`);
551
+ }
552
+ function parsePackageJson(content, filePath) {
553
+ try {
554
+ const parsed = JSON.parse(content);
555
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
556
+ throw new Error("package.json must contain an object.");
557
+ }
558
+ return parsed;
559
+ } catch (cause) {
560
+ throw new Error(
561
+ `Failed to parse package.json at ${filePath}: ${cause instanceof Error ? cause.message : String(cause)}`
562
+ );
563
+ }
564
+ }
565
+ function isNextJsProject(packageJson) {
566
+ const dependencies = packageJson.dependencies;
567
+ const devDependencies = packageJson.devDependencies;
568
+ return hasPackage(dependencies, "next") || hasPackage(devDependencies, "next");
569
+ }
570
+ function hasPackage(section, packageName) {
571
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
572
+ return false;
573
+ }
574
+ return Boolean(section[packageName]);
575
+ }
576
+ function ensureStringMapSection(packageJson, sectionName) {
577
+ const currentValue = packageJson[sectionName];
578
+ if (!currentValue || typeof currentValue !== "object" || Array.isArray(currentValue)) {
579
+ packageJson[sectionName] = {};
580
+ }
581
+ return packageJson[sectionName];
582
+ }
583
+ function toRelativePath(cwd, absolutePath) {
584
+ const relative = path3__default.default.relative(cwd, absolutePath);
585
+ return relative || ".";
586
+ }
13
587
  var __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
14
- var __dirname$1 = path__default.default.dirname(__filename$1);
588
+ var __dirname$1 = path3__default.default.dirname(__filename$1);
589
+ var SKILL_DIRS = ["dataqueue-core", "dataqueue-advanced", "dataqueue-react"];
590
+ function detectAiTools(cwd, existsSync2 = fs__default.default.existsSync) {
591
+ const tools = [];
592
+ const checks = [
593
+ {
594
+ name: "Cursor",
595
+ indicator: ".cursor",
596
+ targetDir: ".cursor/skills"
597
+ },
598
+ {
599
+ name: "Claude Code",
600
+ indicator: ".claude",
601
+ targetDir: ".claude/skills"
602
+ },
603
+ {
604
+ name: "GitHub Copilot",
605
+ indicator: ".github",
606
+ targetDir: ".github/skills"
607
+ }
608
+ ];
609
+ for (const check of checks) {
610
+ if (existsSync2(path3__default.default.join(cwd, check.indicator))) {
611
+ tools.push({ name: check.name, targetDir: check.targetDir });
612
+ }
613
+ }
614
+ return tools;
615
+ }
616
+ function runInstallSkills({
617
+ log = console.log,
618
+ error = console.error,
619
+ exit = (code) => process.exit(code),
620
+ cwd = process.cwd(),
621
+ existsSync: existsSync2 = fs__default.default.existsSync,
622
+ mkdirSync: mkdirSync2 = fs__default.default.mkdirSync,
623
+ copyFileSync = fs__default.default.copyFileSync,
624
+ readdirSync = fs__default.default.readdirSync,
625
+ skillsSourceDir = path3__default.default.join(__dirname$1, "../ai/skills")
626
+ } = {}) {
627
+ const tools = detectAiTools(cwd, existsSync2);
628
+ if (tools.length === 0) {
629
+ log("No AI tool directories detected (.cursor/, .claude/, .github/).");
630
+ log("Creating .cursor/skills/ as the default target.");
631
+ tools.push({ name: "Cursor", targetDir: ".cursor/skills" });
632
+ }
633
+ let installed = 0;
634
+ for (const tool of tools) {
635
+ log(`
636
+ Installing skills for ${tool.name}...`);
637
+ for (const skillDir of SKILL_DIRS) {
638
+ const srcDir = path3__default.default.join(skillsSourceDir, skillDir);
639
+ const destDir = path3__default.default.join(cwd, tool.targetDir, skillDir);
640
+ try {
641
+ mkdirSync2(destDir, { recursive: true });
642
+ const files = readdirSync(srcDir);
643
+ for (const file of files) {
644
+ copyFileSync(path3__default.default.join(srcDir, file), path3__default.default.join(destDir, file));
645
+ }
646
+ log(` \u2713 ${skillDir}`);
647
+ installed++;
648
+ } catch (err) {
649
+ error(` \u2717 Failed to install ${skillDir}:`, err);
650
+ }
651
+ }
652
+ }
653
+ if (installed > 0) {
654
+ log(
655
+ `
656
+ Done! Installed ${installed} skill(s) for ${tools.map((t) => t.name).join(", ")}.`
657
+ );
658
+ } else {
659
+ error("No skills were installed.");
660
+ exit(1);
661
+ }
662
+ }
663
+ var __filename2 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
664
+ var __dirname2 = path3__default.default.dirname(__filename2);
665
+ var RULE_FILES = ["basic.md", "advanced.md", "react-dashboard.md"];
666
+ var MARKER_START = "<!-- DATAQUEUE RULES START -->";
667
+ var MARKER_END = "<!-- DATAQUEUE RULES END -->";
668
+ function upsertMarkedSection(filePath, content, deps) {
669
+ const block = `${MARKER_START}
670
+ ${content}
671
+ ${MARKER_END}`;
672
+ if (!deps.existsSync(filePath)) {
673
+ deps.writeFileSync(filePath, block + "\n");
674
+ return;
675
+ }
676
+ const existing = deps.readFileSync(filePath, "utf-8");
677
+ const startIdx = existing.indexOf(MARKER_START);
678
+ const endIdx = existing.indexOf(MARKER_END);
679
+ if (startIdx !== -1 && endIdx !== -1) {
680
+ const before = existing.slice(0, startIdx);
681
+ const after = existing.slice(endIdx + MARKER_END.length);
682
+ deps.writeFileSync(filePath, before + block + after);
683
+ } else {
684
+ deps.writeFileSync(filePath, existing.trimEnd() + "\n\n" + block + "\n");
685
+ }
686
+ }
687
+ function getAllRulesContent(rulesSourceDir, readFileSync2) {
688
+ return RULE_FILES.map(
689
+ (f) => readFileSync2(path3__default.default.join(rulesSourceDir, f), "utf-8")
690
+ ).join("\n\n");
691
+ }
692
+ var CLIENTS = {
693
+ "1": {
694
+ label: "Cursor",
695
+ install: (deps) => {
696
+ const rulesDir = path3__default.default.join(deps.cwd, ".cursor", "rules");
697
+ deps.mkdirSync(rulesDir, { recursive: true });
698
+ for (const file of RULE_FILES) {
699
+ const src = deps.readFileSync(
700
+ path3__default.default.join(deps.rulesSourceDir, file),
701
+ "utf-8"
702
+ );
703
+ const destName = `dataqueue-${file.replace(/\.md$/, ".mdc")}`;
704
+ deps.writeFileSync(path3__default.default.join(rulesDir, destName), src);
705
+ deps.log(` \u2713 .cursor/rules/${destName}`);
706
+ }
707
+ }
708
+ },
709
+ "2": {
710
+ label: "Claude Code",
711
+ install: (deps) => {
712
+ const content = getAllRulesContent(
713
+ deps.rulesSourceDir,
714
+ deps.readFileSync
715
+ );
716
+ const filePath = path3__default.default.join(deps.cwd, "CLAUDE.md");
717
+ upsertMarkedSection(filePath, content, deps);
718
+ deps.log(` \u2713 CLAUDE.md`);
719
+ }
720
+ },
721
+ "3": {
722
+ label: "AGENTS.md (Codex, Jules, OpenCode)",
723
+ install: (deps) => {
724
+ const content = getAllRulesContent(
725
+ deps.rulesSourceDir,
726
+ deps.readFileSync
727
+ );
728
+ const filePath = path3__default.default.join(deps.cwd, "AGENTS.md");
729
+ upsertMarkedSection(filePath, content, deps);
730
+ deps.log(` \u2713 AGENTS.md`);
731
+ }
732
+ },
733
+ "4": {
734
+ label: "GitHub Copilot",
735
+ install: (deps) => {
736
+ const content = getAllRulesContent(
737
+ deps.rulesSourceDir,
738
+ deps.readFileSync
739
+ );
740
+ deps.mkdirSync(path3__default.default.join(deps.cwd, ".github"), { recursive: true });
741
+ const filePath = path3__default.default.join(
742
+ deps.cwd,
743
+ ".github",
744
+ "copilot-instructions.md"
745
+ );
746
+ upsertMarkedSection(filePath, content, deps);
747
+ deps.log(` \u2713 .github/copilot-instructions.md`);
748
+ }
749
+ },
750
+ "5": {
751
+ label: "Windsurf",
752
+ install: (deps) => {
753
+ const content = getAllRulesContent(
754
+ deps.rulesSourceDir,
755
+ deps.readFileSync
756
+ );
757
+ const filePath = path3__default.default.join(deps.cwd, "CONVENTIONS.md");
758
+ upsertMarkedSection(filePath, content, deps);
759
+ deps.log(` \u2713 CONVENTIONS.md`);
760
+ }
761
+ }
762
+ };
763
+ async function runInstallRules({
764
+ log = console.log,
765
+ error = console.error,
766
+ exit = (code) => process.exit(code),
767
+ cwd = process.cwd(),
768
+ readFileSync: readFileSync2 = fs__default.default.readFileSync,
769
+ writeFileSync: writeFileSync2 = fs__default.default.writeFileSync,
770
+ appendFileSync = fs__default.default.appendFileSync,
771
+ mkdirSync: mkdirSync2 = fs__default.default.mkdirSync,
772
+ existsSync: existsSync2 = fs__default.default.existsSync,
773
+ rulesSourceDir = path3__default.default.join(__dirname2, "../ai/rules"),
774
+ selectedClient
775
+ } = {}) {
776
+ log("DataQueue Agent Rules Installer\n");
777
+ log("Select your AI client:\n");
778
+ for (const [key, client2] of Object.entries(CLIENTS)) {
779
+ log(` ${key}) ${client2.label}`);
780
+ }
781
+ log("");
782
+ let choice = selectedClient;
783
+ if (!choice) {
784
+ const rl = readline__default.default.createInterface({
785
+ input: process.stdin,
786
+ output: process.stdout
787
+ });
788
+ choice = await new Promise((resolve) => {
789
+ rl.question("Enter choice (1-5): ", (answer) => {
790
+ rl.close();
791
+ resolve(answer.trim());
792
+ });
793
+ });
794
+ }
795
+ const client = CLIENTS[choice];
796
+ if (!client) {
797
+ error(`Invalid choice: "${choice}". Expected 1-5.`);
798
+ exit(1);
799
+ return;
800
+ }
801
+ log(`
802
+ Installing rules for ${client.label}...`);
803
+ try {
804
+ client.install({
805
+ cwd,
806
+ readFileSync: readFileSync2,
807
+ writeFileSync: writeFileSync2,
808
+ appendFileSync,
809
+ mkdirSync: mkdirSync2,
810
+ existsSync: existsSync2,
811
+ log,
812
+ rulesSourceDir
813
+ });
814
+ log("\nDone!");
815
+ } catch (err) {
816
+ error("Failed to install rules:", err);
817
+ exit(1);
818
+ }
819
+ }
820
+ function upsertMcpConfig(filePath, serverKey, serverConfig, deps) {
821
+ let config = {};
822
+ if (deps.existsSync(filePath)) {
823
+ try {
824
+ config = JSON.parse(deps.readFileSync(filePath, "utf-8"));
825
+ } catch {
826
+ config = {};
827
+ }
828
+ }
829
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
830
+ config.mcpServers = {};
831
+ }
832
+ config.mcpServers[serverKey] = serverConfig;
833
+ deps.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
834
+ }
835
+ var MCP_SERVER_CONFIG = {
836
+ command: "npx",
837
+ args: ["dataqueue-cli", "mcp"]
838
+ };
839
+ var MCP_CLIENTS = {
840
+ "1": {
841
+ label: "Cursor",
842
+ install: (deps) => {
843
+ const configDir = path3__default.default.join(deps.cwd, ".cursor");
844
+ deps.mkdirSync(configDir, { recursive: true });
845
+ const configFile = path3__default.default.join(configDir, "mcp.json");
846
+ upsertMcpConfig(configFile, "dataqueue", MCP_SERVER_CONFIG, deps);
847
+ deps.log(` \u2713 .cursor/mcp.json`);
848
+ }
849
+ },
850
+ "2": {
851
+ label: "Claude Code",
852
+ install: (deps) => {
853
+ const configFile = path3__default.default.join(deps.cwd, ".mcp.json");
854
+ upsertMcpConfig(configFile, "dataqueue", MCP_SERVER_CONFIG, deps);
855
+ deps.log(` \u2713 .mcp.json`);
856
+ }
857
+ },
858
+ "3": {
859
+ label: "VS Code (Copilot)",
860
+ install: (deps) => {
861
+ const configDir = path3__default.default.join(deps.cwd, ".vscode");
862
+ deps.mkdirSync(configDir, { recursive: true });
863
+ const configFile = path3__default.default.join(configDir, "mcp.json");
864
+ upsertMcpConfig(configFile, "dataqueue", MCP_SERVER_CONFIG, deps);
865
+ deps.log(` \u2713 .vscode/mcp.json`);
866
+ }
867
+ },
868
+ "4": {
869
+ label: "Windsurf",
870
+ install: (deps) => {
871
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
872
+ const configFile = path3__default.default.join(
873
+ homeDir,
874
+ ".codeium",
875
+ "windsurf",
876
+ "mcp_config.json"
877
+ );
878
+ deps.mkdirSync(path3__default.default.dirname(configFile), { recursive: true });
879
+ upsertMcpConfig(configFile, "dataqueue", MCP_SERVER_CONFIG, deps);
880
+ deps.log(` \u2713 ~/.codeium/windsurf/mcp_config.json`);
881
+ }
882
+ }
883
+ };
884
+ async function runInstallMcp({
885
+ log = console.log,
886
+ error = console.error,
887
+ exit = (code) => process.exit(code),
888
+ cwd = process.cwd(),
889
+ readFileSync: readFileSync2 = fs__default.default.readFileSync,
890
+ writeFileSync: writeFileSync2 = fs__default.default.writeFileSync,
891
+ mkdirSync: mkdirSync2 = fs__default.default.mkdirSync,
892
+ existsSync: existsSync2 = fs__default.default.existsSync,
893
+ selectedClient
894
+ } = {}) {
895
+ log("DataQueue MCP Server Installer\n");
896
+ log("Select your AI client:\n");
897
+ for (const [key, client2] of Object.entries(MCP_CLIENTS)) {
898
+ log(` ${key}) ${client2.label}`);
899
+ }
900
+ log("");
901
+ let choice = selectedClient;
902
+ if (!choice) {
903
+ const rl = readline__default.default.createInterface({
904
+ input: process.stdin,
905
+ output: process.stdout
906
+ });
907
+ choice = await new Promise((resolve) => {
908
+ rl.question("Enter choice (1-4): ", (answer) => {
909
+ rl.close();
910
+ resolve(answer.trim());
911
+ });
912
+ });
913
+ }
914
+ const client = MCP_CLIENTS[choice];
915
+ if (!client) {
916
+ error(`Invalid choice: "${choice}". Expected 1-4.`);
917
+ exit(1);
918
+ return;
919
+ }
920
+ log(`
921
+ Installing MCP config for ${client.label}...`);
922
+ try {
923
+ client.install({
924
+ cwd,
925
+ readFileSync: readFileSync2,
926
+ writeFileSync: writeFileSync2,
927
+ mkdirSync: mkdirSync2,
928
+ existsSync: existsSync2,
929
+ log
930
+ });
931
+ log("\nDone! The MCP server will run via: npx dataqueue-cli mcp");
932
+ } catch (err) {
933
+ error("Failed to install MCP config:", err);
934
+ exit(1);
935
+ }
936
+ }
937
+ var __filename3 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
938
+ var __dirname3 = path3__default.default.dirname(__filename3);
939
+ function loadDocsContent(docsPath = path3__default.default.join(__dirname3, "../ai/docs-content.json")) {
940
+ const raw = fs__default.default.readFileSync(docsPath, "utf-8");
941
+ return JSON.parse(raw);
942
+ }
943
+ function scorePageForQuery(page, queryTerms) {
944
+ const titleLower = page.title.toLowerCase();
945
+ const descLower = page.description.toLowerCase();
946
+ const contentLower = page.content.toLowerCase();
947
+ let score = 0;
948
+ for (const term of queryTerms) {
949
+ if (titleLower.includes(term)) score += 10;
950
+ if (descLower.includes(term)) score += 5;
951
+ const contentMatches = contentLower.split(term).length - 1;
952
+ score += Math.min(contentMatches, 10);
953
+ }
954
+ return score;
955
+ }
956
+ function extractExcerpt(content, queryTerms, maxLength = 500) {
957
+ const lower = content.toLowerCase();
958
+ let earliestIndex = -1;
959
+ for (const term of queryTerms) {
960
+ const idx = lower.indexOf(term);
961
+ if (idx !== -1 && (earliestIndex === -1 || idx < earliestIndex)) {
962
+ earliestIndex = idx;
963
+ }
964
+ }
965
+ if (earliestIndex === -1) {
966
+ return content.slice(0, maxLength);
967
+ }
968
+ const start = Math.max(0, earliestIndex - 100);
969
+ const end = Math.min(content.length, start + maxLength);
970
+ let excerpt = content.slice(start, end);
971
+ if (start > 0) excerpt = "..." + excerpt;
972
+ if (end < content.length) excerpt = excerpt + "...";
973
+ return excerpt;
974
+ }
975
+ async function startMcpServer(deps = {}) {
976
+ const pages = loadDocsContent(deps.docsPath);
977
+ const server = new mcp_js.McpServer({
978
+ name: "dataqueue-docs",
979
+ version: "1.0.0"
980
+ });
981
+ server.resource("llms-txt", "dataqueue://llms.txt", async () => {
982
+ const llmsPath = path3__default.default.join(
983
+ __dirname3,
984
+ "../ai/skills/dataqueue-core/SKILL.md"
985
+ );
986
+ let content;
987
+ try {
988
+ content = fs__default.default.readFileSync(llmsPath, "utf-8");
989
+ } catch {
990
+ content = pages.map((p) => `## ${p.title}
991
+
992
+ Slug: ${p.slug}
993
+
994
+ ${p.description}`).join("\n\n");
995
+ }
996
+ return { contents: [{ uri: "dataqueue://llms.txt", text: content }] };
997
+ });
998
+ server.tool(
999
+ "list-doc-pages",
1000
+ "List all available DataQueue documentation pages with titles and descriptions.",
1001
+ {},
1002
+ async () => {
1003
+ const listing = pages.map((p) => ({
1004
+ slug: p.slug,
1005
+ title: p.title,
1006
+ description: p.description
1007
+ }));
1008
+ return {
1009
+ content: [
1010
+ { type: "text", text: JSON.stringify(listing, null, 2) }
1011
+ ]
1012
+ };
1013
+ }
1014
+ );
1015
+ server.tool(
1016
+ "get-doc-page",
1017
+ "Fetch a specific DataQueue doc page by slug. Returns full page content as markdown.",
1018
+ {
1019
+ slug: zod.z.string().describe('The doc page slug, e.g. "usage/add-job" or "api/job-queue"')
1020
+ },
1021
+ async ({ slug }) => {
1022
+ const page = pages.find((p) => p.slug === slug);
1023
+ if (!page) {
1024
+ return {
1025
+ content: [
1026
+ {
1027
+ type: "text",
1028
+ text: `Page not found: "${slug}". Use list-doc-pages to see available slugs.`
1029
+ }
1030
+ ],
1031
+ isError: true
1032
+ };
1033
+ }
1034
+ const header = page.description ? `# ${page.title}
1035
+
1036
+ > ${page.description}
1037
+
1038
+ ` : `# ${page.title}
1039
+
1040
+ `;
1041
+ return {
1042
+ content: [{ type: "text", text: header + page.content }]
1043
+ };
1044
+ }
1045
+ );
1046
+ server.tool(
1047
+ "search-docs",
1048
+ "Full-text search across all DataQueue documentation pages. Returns matching sections with page titles and content excerpts.",
1049
+ {
1050
+ query: zod.z.string().describe('Search query, e.g. "cron scheduling" or "waitForToken"')
1051
+ },
1052
+ async ({ query }) => {
1053
+ const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
1054
+ if (queryTerms.length === 0) {
1055
+ return {
1056
+ content: [
1057
+ { type: "text", text: "Please provide a search query." }
1058
+ ],
1059
+ isError: true
1060
+ };
1061
+ }
1062
+ const scored = pages.map((page) => ({
1063
+ page,
1064
+ score: scorePageForQuery(page, queryTerms)
1065
+ })).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, 5);
1066
+ if (scored.length === 0) {
1067
+ return {
1068
+ content: [
1069
+ {
1070
+ type: "text",
1071
+ text: `No results for "${query}". Try different keywords or use list-doc-pages to browse.`
1072
+ }
1073
+ ]
1074
+ };
1075
+ }
1076
+ const results = scored.map((r) => {
1077
+ const excerpt = extractExcerpt(r.page.content, queryTerms);
1078
+ return `## ${r.page.title} (${r.page.slug})
1079
+
1080
+ ${r.page.description}
1081
+
1082
+ ${excerpt}`;
1083
+ });
1084
+ return {
1085
+ content: [{ type: "text", text: results.join("\n\n---\n\n") }]
1086
+ };
1087
+ }
1088
+ );
1089
+ const transport = deps.transport ?? new stdio_js.StdioServerTransport();
1090
+ await server.connect(transport);
1091
+ return server;
1092
+ }
1093
+ var isDirectRun = process.argv[1] && (process.argv[1].endsWith("/mcp-server.js") || process.argv[1].endsWith("/mcp-server.cjs"));
1094
+ if (isDirectRun) {
1095
+ startMcpServer().catch((err) => {
1096
+ console.error("Failed to start MCP server:", err);
1097
+ process.exit(1);
1098
+ });
1099
+ }
1100
+
1101
+ // src/cli.ts
1102
+ var __filename4 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
1103
+ var __dirname4 = path3__default.default.dirname(__filename4);
15
1104
  function runCli(argv, {
16
1105
  log = console.log,
1106
+ error = console.error,
17
1107
  exit = (code) => process.exit(code),
18
1108
  spawnSyncImpl = child_process.spawnSync,
19
- migrationsDir = path__default.default.join(__dirname$1, "../migrations")
1109
+ migrationsDir = path3__default.default.join(__dirname4, "../migrations"),
1110
+ initDeps,
1111
+ runInitImpl = runInit,
1112
+ installSkillsDeps,
1113
+ runInstallSkillsImpl = runInstallSkills,
1114
+ installRulesDeps,
1115
+ runInstallRulesImpl = runInstallRules,
1116
+ installMcpDeps,
1117
+ runInstallMcpImpl = runInstallMcp,
1118
+ startMcpServerImpl = startMcpServer
20
1119
  } = {}) {
21
1120
  const [, , command, ...restArgs] = argv;
22
1121
  function printUsage() {
1122
+ log("Usage:");
23
1123
  log(
24
- "Usage: dataqueue-cli migrate [--envPath <path>] [-s <schema> | --schema <schema>]"
1124
+ " dataqueue-cli migrate [--envPath <path>] [-s <schema> | --schema <schema>]"
25
1125
  );
1126
+ log(" dataqueue-cli init");
1127
+ log(" dataqueue-cli install-skills");
1128
+ log(" dataqueue-cli install-rules");
1129
+ log(" dataqueue-cli install-mcp");
1130
+ log(" dataqueue-cli mcp");
26
1131
  log("");
27
- log("Options:");
1132
+ log("Options for migrate:");
28
1133
  log(
29
1134
  " --envPath <path> Path to a .env file to load environment variables (passed to node-pg-migrate)"
30
1135
  );
@@ -32,16 +1137,13 @@ function runCli(argv, {
32
1137
  " -s, --schema <schema> Set the schema to use (passed to node-pg-migrate)"
33
1138
  );
34
1139
  log("");
35
- log("Notes:");
36
- log(
37
- " - The PG_DATAQUEUE_DATABASE environment variable must be set to your Postgres connection string."
38
- );
39
- log(
40
- " - For managed Postgres (e.g., DigitalOcean) with SSL, set PGSSLMODE=require and PGSSLROOTCERT to your CA .crt file."
41
- );
1140
+ log("AI tooling commands:");
1141
+ log(" install-skills Install DataQueue skill files for AI assistants");
1142
+ log(" install-rules Install DataQueue agent rules for AI clients");
42
1143
  log(
43
- " Example: PGSSLMODE=require NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.crt PG_DATAQUEUE_DATABASE=... npx dataqueue-cli migrate"
1144
+ " install-mcp Configure the DataQueue MCP server for AI clients"
44
1145
  );
1146
+ log(" mcp Start the DataQueue MCP server (stdio)");
45
1147
  exit(1);
46
1148
  }
47
1149
  if (command === "migrate") {
@@ -78,6 +1180,39 @@ function runCli(argv, {
78
1180
  { stdio: "inherit" }
79
1181
  );
80
1182
  exit(result.status ?? 1);
1183
+ } else if (command === "init") {
1184
+ runInitImpl({
1185
+ log,
1186
+ error,
1187
+ exit,
1188
+ ...initDeps
1189
+ });
1190
+ } else if (command === "install-skills") {
1191
+ runInstallSkillsImpl({
1192
+ log,
1193
+ error,
1194
+ exit,
1195
+ ...installSkillsDeps
1196
+ });
1197
+ } else if (command === "install-rules") {
1198
+ runInstallRulesImpl({
1199
+ log,
1200
+ error,
1201
+ exit,
1202
+ ...installRulesDeps
1203
+ });
1204
+ } else if (command === "install-mcp") {
1205
+ runInstallMcpImpl({
1206
+ log,
1207
+ error,
1208
+ exit,
1209
+ ...installMcpDeps
1210
+ });
1211
+ } else if (command === "mcp") {
1212
+ startMcpServerImpl().catch((err) => {
1213
+ error("Failed to start MCP server:", err);
1214
+ exit(1);
1215
+ });
81
1216
  } else {
82
1217
  printUsage();
83
1218
  }