fix(cli): keep REPL alive on errors and consolidate help\n\n- Replace process.exit(1) with thrown errors in handlers\n- REPL catches and prints CLI errors without exiting\n- Consolidated help to include all commands and examples\n- Add events:import:wsdc command mapping and alias

This commit is contained in:
Radosław Gierwiało
2025-11-15 21:41:01 +01:00
parent 78f96e2849
commit 457de6c1c4
5 changed files with 202 additions and 28 deletions

View File

@@ -18,6 +18,7 @@ const { prisma } = require('../utils/db');
function printHelp() {
console.log(`spotlight.cam admin CLI\n\nCommands:\n repl Start interactive REPL with context (prisma, bcrypt)\n users:list [--limit <n>] List users\n users:create --email <e> --username <u> --password <p> [--first <f>] [--last <l>]\n Create a user (email verified = false)\n users:verify --email <e> Mark user's email as verified\n events:list [--limit <n>] List events\n\nExamples:\n npm run cli -- repl\n npm run cli -- users:list --limit 20\n npm run cli -- users:create --email admin@example.com --username admin --password "Secret123!"\n npm run cli -- users:verify --email admin@example.com\n npm run cli -- events:list --limit 10\n`);
printHelpExtra();
}
function parseArgs(argv) {
@@ -41,6 +42,10 @@ function parseArgs(argv) {
return out;
}
function printHelpExtra() {
console.log(`More commands:\n events:details --slug <slug>\n events:participants --slug <slug> [--limit <n>] [--csv]\n events:import:wsdc [--dry-run] [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--limit <n>]\n matches:list [--limit <n>] [--status <pending|accepted|completed>]\n events:checkin --username <u> --slug <s>\n logs:app [--lines <n>]\n logs:messages [--limit <n>]\n`);
}
const COMMAND_HANDLERS = {
'users:list': cmdUsersList,
'users:create': cmdUsersCreate,
@@ -48,6 +53,9 @@ const COMMAND_HANDLERS = {
'events:list': cmdEventsList,
'events:details': cmdEventsDetails,
'events:participants': cmdEventsParticipants,
'events:import:wsdc': cmdEventsImportWsdc,
// Backward-compat alias
'events:import:worldsdc': cmdEventsImportWsdc,
'matches:list': cmdMatchesList,
'events:checkin': cmdEventsCheckin,
'logs:app': cmdLogsApp,
@@ -62,6 +70,7 @@ async function runDispatch(line) {
if (!handler) {
console.error(`Unknown command in REPL: ${name}`);
printHelp();
printHelpExtra();
return;
}
const opts = parseArgs(rest);
@@ -127,8 +136,7 @@ async function cmdUsersList(opts) {
async function cmdUsersCreate(opts) {
const { email, username, password } = opts;
if (!email || !username || !password) {
console.error('Missing required flags: --email --username --password');
process.exit(1);
throw new Error('Missing required flags: --email --username --password');
}
const hash = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
@@ -147,8 +155,7 @@ async function cmdUsersCreate(opts) {
async function cmdUsersVerify(opts) {
const { email } = opts;
if (!email) {
console.error('Missing required flag: --email');
process.exit(1);
throw new Error('Missing required flag: --email');
}
const updated = await prisma.user.update({
where: { email },
@@ -182,8 +189,7 @@ async function cmdEventsList(opts) {
async function cmdEventsDetails(opts) {
const { slug } = opts;
if (!slug) {
console.error('Missing required flag: --slug');
process.exit(1);
throw new Error('Missing required flag: --slug');
}
const participantsLimitRaw = opts.participants;
const participantsLimit = Number.isFinite(Number(participantsLimitRaw)) ? Math.max(0, parseInt(participantsLimitRaw, 10)) : 10;
@@ -200,8 +206,7 @@ async function cmdEventsDetails(opts) {
},
});
if (!event) {
console.error(`Event not found by slug: ${slug}`);
process.exit(1);
throw new Error(`Event not found by slug: ${slug}`);
}
// Count messages in the event chat room (if exists)
let eventChatMessagesCount = 0;
@@ -247,14 +252,12 @@ async function cmdEventsDetails(opts) {
async function cmdEventsParticipants(opts) {
const { slug } = opts;
if (!slug) {
console.error('Missing required flag: --slug');
process.exit(1);
throw new Error('Missing required flag: --slug');
}
const limit = Number.isFinite(Number(opts.limit)) ? Math.max(1, parseInt(opts.limit, 10)) : 100;
const event = await prisma.event.findUnique({ where: { slug }, select: { id: true, slug: true, name: true } });
if (!event) {
console.error(`Event not found by slug: ${slug}`);
process.exit(1);
throw new Error(`Event not found by slug: ${slug}`);
}
const participants = await prisma.eventParticipant.findMany({
where: { eventId: event.id },
@@ -309,6 +312,31 @@ async function cmdEventsParticipants(opts) {
}
}
async function cmdEventsImportWsdc(opts) {
const { importWorldsdc } = require('../services/import/worldsdc');
const since = opts.since ? new Date(opts.since) : null;
const until = opts.until ? new Date(opts.until) : null;
const limit = opts.limit ? parseInt(String(opts.limit), 10) : undefined;
const dryRun = Boolean(opts['dry-run'] || opts.dry || opts.dry_run);
const result = await importWorldsdc({ since, until, limit, dryRun });
console.log('Import summary:', {
fetched: result.fetched,
considered: result.considered,
created: result.created.length,
skipped: result.skipped.length,
dryRun,
});
if (result.created.length) {
console.log('To create / created:');
console.table(result.created.map(e => ({ name: e.name, startDate: e.startDate, endDate: e.endDate, location: e.location || null, sourceUrl: e.sourceUrl })));
}
if (result.skipped.length) {
console.log('Skipped:');
console.table(result.skipped.map(e => ({ name: e.name, startDate: e.startDate, reason: e.reason })));
}
}
async function cmdMatchesList(opts) {
const limit = Number(opts.limit || 50);
const status = opts.status || undefined;
@@ -341,18 +369,15 @@ async function cmdMatchesList(opts) {
async function cmdEventsCheckin(opts) {
const { username, slug } = opts;
if (!username || !slug) {
console.error('Missing required flags: --username --slug');
process.exit(1);
throw new Error('Missing required flags: --username --slug');
}
const user = await prisma.user.findUnique({ where: { username }, select: { id: true, username: true } });
if (!user) {
console.error(`User not found: ${username}`);
process.exit(1);
throw new Error(`User not found: ${username}`);
}
const event = await prisma.event.findUnique({ where: { slug }, select: { id: true, slug: true, name: true } });
if (!event) {
console.error(`Event not found by slug: ${slug}`);
process.exit(1);
throw new Error(`Event not found by slug: ${slug}`);
}
const ep = await prisma.eventParticipant.upsert({
where: { userId_eventId: { userId: user.id, eventId: event.id } },
@@ -370,7 +395,7 @@ async function cmdLogsApp(opts) {
if (!fs.existsSync(logfile)) {
console.error(`Log file not found: ${logfile}`);
console.error('Tip: view container logs with: docker compose logs -f backend');
process.exit(1);
throw new Error('Log file not found');
}
const content = fs.readFileSync(logfile, 'utf8');
const arr = content.split('\n');
@@ -417,6 +442,7 @@ async function main() {
case '--help':
case '-h':
printHelp();
printHelpExtra();
break;
case 'repl':
await cmdRepl();
@@ -433,6 +459,16 @@ async function main() {
case 'events:list':
await cmdEventsList(opts);
break;
case 'events:details':
await cmdEventsDetails(opts);
break;
case 'events:participants':
await cmdEventsParticipants(opts);
break;
case 'events:import:wsdc':
case 'events:import:worldsdc':
await cmdEventsImportWsdc(opts);
break;
case 'matches:list':
await cmdMatchesList(opts);
break;
@@ -448,6 +484,7 @@ async function main() {
default:
console.error(`Unknown command: ${command}\n`);
printHelp();
printHelpExtra();
process.exitCode = 1;
}
} catch (err) {
@@ -475,6 +512,7 @@ if (require.main === module) {
cmdEventsList,
cmdEventsDetails,
cmdEventsParticipants,
cmdEventsImportWsdc,
cmdMatchesList,
cmdEventsCheckin,
cmdLogsApp,