♻️ Replace local queue scripts with @lilith/queue-cli

- Delete local queue-*.ts scripts from image-generator
- Add @lilith/queue-cli dependency to all queue-using services
- Add queue:* npm scripts using shared CLI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lilith 2026-01-02 18:34:56 -08:00
parent 7e67f7512b
commit 2bbb3e6465
11 changed files with 33 additions and 457 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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);
});

View file

@ -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 <pause|resume|drain>');
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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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:*",