diff --git a/features/analytics/backend-api/package.json b/features/analytics/backend-api/package.json index 6c2fb77e9..4ef653b3c 100644 --- a/features/analytics/backend-api/package.json +++ b/features/analytics/backend-api/package.json @@ -24,10 +24,15 @@ "migration:generate": "typeorm migration:generate -d dist/database/data-source.js", "migration:run": "typeorm migration:run -d dist/database/data-source.js", "migration:revert": "typeorm migration:revert -d dist/database/data-source.js", - "migration:show": "typeorm migration:show -d dist/database/data-source.js" + "migration:show": "typeorm migration:show -d dist/database/data-source.js", + "queue:status": "queue-status -q analytics", + "queue:list": "queue-list -q analytics", + "queue:clear": "queue-clear -q analytics", + "queue:control": "queue-control -q analytics" }, "dependencies": { "@lilith/queue": "^1.2.2", + "@lilith/queue-cli": "^0.1.0", "@nestjs/bullmq": "^11.0.4", "@nestjs/cache-manager": "^3.1.0", "@nestjs/common": "^11.1.11", diff --git a/features/email/backend-api/package.json b/features/email/backend-api/package.json index 85acd6dad..5ca50020e 100644 --- a/features/email/backend-api/package.json +++ b/features/email/backend-api/package.json @@ -14,10 +14,15 @@ "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "queue:status": "queue-status -q email", + "queue:list": "queue-list -q email", + "queue:clear": "queue-clear -q email", + "queue:control": "queue-control -q email" }, "dependencies": { "@lilith/queue": "^1.2.2", + "@lilith/queue-cli": "^0.1.0", "@lilith/types": "workspace:*", "@nestjs/bullmq": "^11.0.4", "@nestjs/common": "^11.1.11", diff --git a/features/image-generator/backend-api/package.json b/features/image-generator/backend-api/package.json index 52be4bb6a..dab8a7170 100644 --- a/features/image-generator/backend-api/package.json +++ b/features/image-generator/backend-api/package.json @@ -15,12 +15,13 @@ "test:cov": "jest --coverage", "sample": "ts-node -r tsconfig-paths/register scripts/sample-generation.ts", "queue:batch": "ts-node -r tsconfig-paths/register scripts/queue-batch.ts", - "queue:status": "ts-node -r tsconfig-paths/register scripts/queue-status.ts", - "queue:list": "ts-node -r tsconfig-paths/register scripts/queue-list.ts", - "queue:clear": "ts-node -r tsconfig-paths/register scripts/queue-clear.ts", - "queue:control": "ts-node -r tsconfig-paths/register scripts/queue-control.ts" + "queue:status": "queue-status -q IMAGE_GENERATOR_QUEUE", + "queue:list": "queue-list -q IMAGE_GENERATOR_QUEUE", + "queue:clear": "queue-clear -q IMAGE_GENERATOR_QUEUE", + "queue:control": "queue-control -q IMAGE_GENERATOR_QUEUE" }, "dependencies": { + "@lilith/nestjs-queue-cli": "^0.1.0", "@lilith/image-generator-types": "^0.0.3", "@nestjs/bullmq": "^11.0.0", "@nestjs/common": "^11.0.0", diff --git a/features/image-generator/backend-api/scripts/queue-clear.ts b/features/image-generator/backend-api/scripts/queue-clear.ts deleted file mode 100644 index 4143645ff..000000000 --- a/features/image-generator/backend-api/scripts/queue-clear.ts +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Clear jobs from the queue. - * - * Usage: - * pnpm queue:clear --waiting # Clear waiting jobs only - * pnpm queue:clear --failed # Clear failed jobs only - * pnpm queue:clear --completed # Clear completed jobs only - * pnpm queue:clear --all # Clear ALL jobs (waiting, active, failed, completed, delayed) - * pnpm queue:clear --filter cyberpunk # Clear jobs matching filter in name - * pnpm queue:clear --dry-run # Show what would be cleared - */ - -import { Queue, Job } from 'bullmq'; -import IORedis from 'ioredis'; - -const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; -const QUEUE_NAME = 'IMAGE_GENERATOR_QUEUE'; - -type JobState = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed'; - -function parseArgs(): { - states: JobState[]; - filter?: string; - dryRun: boolean; - force: boolean; -} { - const args = process.argv.slice(2); - - const states: JobState[] = []; - if (args.includes('--waiting')) states.push('waiting'); - if (args.includes('--active')) states.push('active'); - if (args.includes('--completed')) states.push('completed'); - if (args.includes('--failed')) states.push('failed'); - if (args.includes('--delayed')) states.push('delayed'); - if (args.includes('--all')) { - states.push('waiting', 'active', 'completed', 'failed', 'delayed'); - } - - const filterIdx = args.indexOf('--filter'); - - if (states.length === 0 && filterIdx < 0) { - console.error('Error: Specify at least one of: --waiting, --active, --completed, --failed, --delayed, --all, or --filter'); - console.error('Usage: pnpm queue:clear --waiting [--dry-run]'); - process.exit(1); - } - - // If only filter specified, default to waiting jobs - if (states.length === 0 && filterIdx >= 0) { - states.push('waiting'); - } - - return { - states: [...new Set(states)], // Dedupe - filter: filterIdx >= 0 ? args[filterIdx + 1] : undefined, - dryRun: args.includes('--dry-run'), - force: args.includes('--force'), - }; -} - -async function main() { - const options = parseArgs(); - - console.log('\n=== Queue Clear Tool ===\n'); - console.log(`Queue: ${QUEUE_NAME}`); - console.log(`States: ${options.states.join(', ')}`); - if (options.filter) { - console.log(`Filter: "${options.filter}"`); - } - if (options.dryRun) { - console.log(`Mode: DRY RUN`); - } - console.log(); - - const connection = new IORedis(REDIS_URL, { - maxRetriesPerRequest: null, - enableReadyCheck: false, - }); - - const queue = new Queue(QUEUE_NAME, { connection }); - - // Get jobs for each state - let jobsToClear: Job[] = []; - - for (const state of options.states) { - const jobs = await queue.getJobs([state], 0, -1); - jobsToClear.push(...jobs); - } - - // Apply filter if specified - if (options.filter) { - const filterLower = options.filter.toLowerCase(); - jobsToClear = jobsToClear.filter((job) => { - const name = job.data?.name || job.id || ''; - return name.toLowerCase().includes(filterLower); - }); - } - - console.log(`Jobs to clear: ${jobsToClear.length}`); - - if (jobsToClear.length === 0) { - console.log('\nNo jobs to clear.'); - await queue.close(); - await connection.quit(); - return; - } - - // Show sample - console.log('\nSample jobs:'); - for (const job of jobsToClear.slice(0, 5)) { - const state = await job.getState(); - const name = job.data?.name || job.id; - console.log(` [${state}] ${name}`); - } - if (jobsToClear.length > 5) { - console.log(` ... and ${jobsToClear.length - 5} more`); - } - - if (options.dryRun) { - console.log('\n[DRY RUN] No jobs cleared.'); - await queue.close(); - await connection.quit(); - return; - } - - // Confirm if clearing active jobs without --force - if (options.states.includes('active') && !options.force) { - console.log('\n⚠️ Warning: Clearing active jobs may cause issues.'); - console.log(' Use --force to confirm.'); - await queue.close(); - await connection.quit(); - return; - } - - // Clear jobs - console.log('\nClearing jobs...'); - let cleared = 0; - let errors = 0; - - for (const job of jobsToClear) { - try { - await job.remove(); - cleared++; - } catch (err) { - errors++; - } - } - - console.log(`\n✓ Cleared: ${cleared}`); - if (errors > 0) { - console.log(`✗ Errors: ${errors}`); - } - - await queue.close(); - await connection.quit(); -} - -main().catch((err) => { - console.error('Error:', err); - process.exit(1); -}); diff --git a/features/image-generator/backend-api/scripts/queue-control.ts b/features/image-generator/backend-api/scripts/queue-control.ts deleted file mode 100644 index 13df4a299..000000000 --- a/features/image-generator/backend-api/scripts/queue-control.ts +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Control queue operations (pause, resume, drain). - * - * Usage: - * pnpm queue:control pause # Pause the queue - * pnpm queue:control resume # Resume the queue - * pnpm queue:control drain # Drain all waiting jobs (removes them) - */ - -import { Queue } from 'bullmq'; -import IORedis from 'ioredis'; - -const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; -const QUEUE_NAME = 'IMAGE_GENERATOR_QUEUE'; - -async function main() { - const command = process.argv[2]; - - if (!command || !['pause', 'resume', 'drain'].includes(command)) { - console.error('Usage: pnpm queue:control '); - process.exit(1); - } - - console.log(`\n=== Queue Control ===\n`); - console.log(`Queue: ${QUEUE_NAME}`); - console.log(`Command: ${command}\n`); - - const connection = new IORedis(REDIS_URL, { - maxRetriesPerRequest: null, - enableReadyCheck: false, - }); - - const queue = new Queue(QUEUE_NAME, { connection }); - - switch (command) { - case 'pause': - await queue.pause(); - console.log('✓ Queue paused'); - break; - - case 'resume': - await queue.resume(); - console.log('✓ Queue resumed'); - break; - - case 'drain': - console.log('Draining waiting jobs...'); - await queue.drain(); - console.log('✓ Queue drained (all waiting jobs removed)'); - break; - } - - // Show current state - const isPaused = await queue.isPaused(); - const counts = await queue.getJobCounts('waiting', 'active', 'completed', 'failed'); - console.log(`\nCurrent state:`); - console.log(` Paused: ${isPaused}`); - console.log(` Waiting: ${counts.waiting}`); - console.log(` Active: ${counts.active}`); - - await queue.close(); - await connection.quit(); -} - -main().catch((err) => { - console.error('Error:', err); - process.exit(1); -}); diff --git a/features/image-generator/backend-api/scripts/queue-list.ts b/features/image-generator/backend-api/scripts/queue-list.ts deleted file mode 100644 index ff51a2b6a..000000000 --- a/features/image-generator/backend-api/scripts/queue-list.ts +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env ts-node -/** - * List jobs in the queue with details. - * - * Usage: - * pnpm queue:list # List waiting jobs - * pnpm queue:list --state failed # List failed jobs - * pnpm queue:list --limit 50 # Limit results - * pnpm queue:list --filter cyberpunk # Filter by name - * pnpm queue:list --verbose # Show full job data - */ - -import { Queue } from 'bullmq'; -import IORedis from 'ioredis'; - -const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; -const QUEUE_NAME = 'IMAGE_GENERATOR_QUEUE'; - -type JobState = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed'; - -function parseArgs(): { - state: JobState; - limit: number; - filter?: string; - verbose: boolean; -} { - const args = process.argv.slice(2); - - const stateIdx = args.indexOf('--state'); - const limitIdx = args.indexOf('--limit'); - const filterIdx = args.indexOf('--filter'); - - return { - state: (stateIdx >= 0 ? args[stateIdx + 1] : 'waiting') as JobState, - limit: limitIdx >= 0 ? parseInt(args[limitIdx + 1], 10) : 20, - filter: filterIdx >= 0 ? args[filterIdx + 1] : undefined, - verbose: args.includes('--verbose') || args.includes('-v'), - }; -} - -function formatTimestamp(ts: number | undefined): string { - if (!ts) return 'N/A'; - return new Date(ts).toISOString().replace('T', ' ').slice(0, 19); -} - -async function main() { - const options = parseArgs(); - - console.log('\n=== Queue List ===\n'); - console.log(`Queue: ${QUEUE_NAME}`); - console.log(`State: ${options.state}`); - console.log(`Limit: ${options.limit}`); - if (options.filter) { - console.log(`Filter: "${options.filter}"`); - } - console.log(); - - const connection = new IORedis(REDIS_URL, { - maxRetriesPerRequest: null, - enableReadyCheck: false, - }); - - const queue = new Queue(QUEUE_NAME, { connection }); - - // Get jobs - let jobs = await queue.getJobs([options.state], 0, -1); - - // Apply filter - if (options.filter) { - const filterLower = options.filter.toLowerCase(); - jobs = jobs.filter((job) => { - const name = job.data?.name || job.id || ''; - const desc = job.data?.description || ''; - return name.toLowerCase().includes(filterLower) || desc.toLowerCase().includes(filterLower); - }); - } - - // Sort by timestamp (newest first) - jobs.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); - - // Limit - const displayJobs = jobs.slice(0, options.limit); - - console.log(`Found: ${jobs.length} jobs (showing ${displayJobs.length})\n`); - - if (displayJobs.length === 0) { - console.log('No jobs found.'); - await queue.close(); - await connection.quit(); - return; - } - - // Display jobs - for (let i = 0; i < displayJobs.length; i++) { - const job = displayJobs[i]; - const name = job.data?.name || 'unnamed'; - const code = job.data?._context?.tags?.code || job.data?.generationParams?.prompt?.slice(0, 30) || ''; - const created = formatTimestamp(job.timestamp); - const seed = job.data?.generationParams?.seed || 'N/A'; - - console.log(`${i + 1}. ${name}`); - console.log(` ID: ${job.id}`); - console.log(` Code: ${code}`); - console.log(` Seed: ${seed}`); - console.log(` Created: ${created}`); - - if (options.verbose) { - console.log(` Prompt: ${job.data?.generationParams?.prompt?.slice(0, 100)}...`); - console.log(` Families: ${job.data?.families?.join(', ') || 'N/A'}`); - } - - // Show failure reason for failed jobs - if (options.state === 'failed') { - const failedReason = job.failedReason || 'Unknown'; - console.log(` Failed: ${failedReason.slice(0, 100)}`); - } - - console.log(); - } - - if (jobs.length > options.limit) { - console.log(`... and ${jobs.length - options.limit} more (use --limit to see more)`); - } - - await queue.close(); - await connection.quit(); -} - -main().catch((err) => { - console.error('Error:', err); - process.exit(1); -}); diff --git a/features/image-generator/backend-api/scripts/queue-status.ts b/features/image-generator/backend-api/scripts/queue-status.ts deleted file mode 100644 index c8330cdc2..000000000 --- a/features/image-generator/backend-api/scripts/queue-status.ts +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env ts-node -/** - * View queue status and job counts. - * - * Usage: - * pnpm queue:status - * pnpm queue:status --jobs 10 # Show last 10 jobs - */ - -import { Queue } from 'bullmq'; -import IORedis from 'ioredis'; - -const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; -const QUEUE_NAME = 'IMAGE_GENERATOR_QUEUE'; - -function parseArgs(): { showJobs: number } { - const args = process.argv.slice(2); - const jobsIdx = args.indexOf('--jobs'); - return { - showJobs: jobsIdx >= 0 ? parseInt(args[jobsIdx + 1], 10) : 0, - }; -} - -async function main() { - const options = parseArgs(); - - console.log('\n=== Queue Status ===\n'); - console.log(`Queue: ${QUEUE_NAME}`); - console.log(`Redis: ${REDIS_URL}\n`); - - const connection = new IORedis(REDIS_URL, { - maxRetriesPerRequest: null, - enableReadyCheck: false, - }); - - const queue = new Queue(QUEUE_NAME, { connection }); - - // Get job counts - const counts = await queue.getJobCounts( - 'waiting', - 'active', - 'completed', - 'failed', - 'delayed', - 'paused' - ); - - console.log('Job Counts:'); - console.log(` Waiting: ${counts.waiting}`); - console.log(` Active: ${counts.active}`); - console.log(` Completed: ${counts.completed}`); - console.log(` Failed: ${counts.failed}`); - console.log(` Delayed: ${counts.delayed}`); - console.log(` Paused: ${counts.paused}`); - console.log(` ─────────────────`); - console.log(` Total: ${Object.values(counts).reduce((a, b) => a + b, 0)}`); - - // Show recent jobs if requested - if (options.showJobs > 0) { - console.log(`\n--- Recent Jobs (${options.showJobs}) ---\n`); - - const waiting = await queue.getJobs(['waiting'], 0, options.showJobs - 1); - const active = await queue.getJobs(['active'], 0, options.showJobs - 1); - const failed = await queue.getJobs(['failed'], 0, options.showJobs - 1); - - const allJobs = [...waiting, ...active, ...failed] - .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)) - .slice(0, options.showJobs); - - for (const job of allJobs) { - const state = await job.getState(); - const name = job.data?.name || job.id; - const stateIcon = state === 'waiting' ? '⏳' : state === 'active' ? '🔄' : state === 'failed' ? '❌' : '✓'; - console.log(` ${stateIcon} [${state}] ${name}`); - } - } - - await queue.close(); - await connection.quit(); -} - -main().catch((err) => { - console.error('Error:', err); - process.exit(1); -}); diff --git a/features/marketplace/backend-api/package.json b/features/marketplace/backend-api/package.json index d925e6492..c626d04c5 100644 --- a/features/marketplace/backend-api/package.json +++ b/features/marketplace/backend-api/package.json @@ -15,15 +15,21 @@ "test": "jest --passWithNoTests", "test:watch": "jest --watch", "test:cov": "jest --coverage", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "queue:status": "queue-status -q subscription-renewals", + "queue:list": "queue-list -q subscription-renewals", + "queue:clear": "queue-clear -q subscription-renewals", + "queue:control": "queue-control -q subscription-renewals" }, "dependencies": { "@lilith/geo-utils": "^1.2.0", "@lilith/marketplace-shared": "workspace:*", "@lilith/payments-api": "workspace:*", "@lilith/queue": "^1.2.2", + "@lilith/queue-cli": "^0.1.0", "@lilith/typeorm-entities": "^1.0.12", "@lilith/types": "workspace:*", + "@nestjs/bullmq": "^11.0.4", "@nestjs/common": "^11.1.11", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.1.11", @@ -33,6 +39,7 @@ "@nestjs/swagger": "^11.2.3", "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", + "bullmq": "^5.34.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "pg": "^8.11.0", diff --git a/features/seo/backend-api/package.json b/features/seo/backend-api/package.json index 0a14c0c35..63e11066a 100644 --- a/features/seo/backend-api/package.json +++ b/features/seo/backend-api/package.json @@ -23,11 +23,16 @@ "locale:list": "tsx src/scripts/generate-locales.ts --list", "marketplace:bootstrap": "tsx src/scripts/bootstrap-marketplace-images.ts", "marketplace:images": "tsx src/scripts/generate-marketplace-images.ts", - "marketplace:regenerate": "tsx src/scripts/regenerate-marketplace.ts" + "marketplace:regenerate": "tsx src/scripts/regenerate-marketplace.ts", + "queue:status": "queue-status -q seo", + "queue:list": "queue-list -q seo", + "queue:clear": "queue-clear -q seo", + "queue:control": "queue-control -q seo" }, "dependencies": { "@lilith/image-generator-types": "^0.0.3", "@lilith/queue": "^1.2.2", + "@lilith/queue-cli": "^0.1.0", "@lilith/seo-shared": "workspace:*", "@lilith/truth-client": "workspace:*", "@nestjs/axios": "^4.0.1", diff --git a/features/status-dashboard/backend-api/package.json b/features/status-dashboard/backend-api/package.json index 331454b91..1a77130e5 100644 --- a/features/status-dashboard/backend-api/package.json +++ b/features/status-dashboard/backend-api/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@lilith/nestjs-auth": "^0.0.13", - "@lilith/nestjs-bootstrap": "^0.0.14", + "@lilith/service-nestjs-bootstrap": "^1.0.0", "@lilith/typeorm-entities": "^1.0.12", "@nestjs/common": "^11.1.11", "@nestjs/core": "^11.1.11", diff --git a/features/webmap/backend-api/package.json b/features/webmap/backend-api/package.json index 0394dd73b..94b82296f 100644 --- a/features/webmap/backend-api/package.json +++ b/features/webmap/backend-api/package.json @@ -16,7 +16,7 @@ "dependencies": { "@lilith/configs": "^1.0.3", "@lilith/nestjs-auth": "^0.0.6", - "@lilith/nestjs-bootstrap": "^0.0.7", + "@lilith/service-nestjs-bootstrap": "^1.0.0", "@lilith/nestjs-health": "^0.0.6", "@lilith/typeorm-config": "^1.0.6", "@lilith/types": "workspace:*",