#!/usr/bin/env node // ───────────────────────────────────────────────────────────── // bin/invite.js — add / remove / list invites, plus grant/revoke // admin on existing invites. // // Usage: // node bin/invite.js add [FirstName] // node bin/invite.js remove // node bin/invite.js list // node bin/invite.js admin add # grant admin // node bin/invite.js admin remove # revoke admin // node bin/invite.js admin list # show all admins // // The first name is optional. When present it's used on the welcome // screen ("Thank you for your interest, Erik."). When absent the // welcome screen falls back to anonymous copy. Re-running `add` on // an existing email updates the first name only; invited_at and // invited_by are preserved. // // Admin flag gates the hidden /admin surface (see server.js). Admin // grant/revoke operates on existing invite rows only — a non-invited // email must be invited first, then promoted. // ───────────────────────────────────────────────────────────── import { q } from '../src/db.js'; const [, , cmd, sub, arg3, arg4] = process.argv; const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; function help() { console.log('Usage:'); console.log(' invite add [FirstName]'); console.log(' invite remove '); console.log(' invite list'); console.log(' invite admin add '); console.log(' invite admin remove '); console.log(' invite admin list'); process.exit(1); } switch (cmd) { case 'add': { const emailArg = sub; const nameArg = arg3; if (!emailArg || !EMAIL_RE.test(emailArg)) help(); const email = emailArg.trim().toLowerCase(); const firstName = nameArg ? nameArg.trim() : null; q.upsertInvite.run(email, firstName, Date.now(), 'cli'); if (firstName) { console.log(`Invited ${email} (${firstName})`); } else { console.log(`Invited ${email}`); } break; } case 'remove': { const emailArg = sub; if (!emailArg || !EMAIL_RE.test(emailArg)) help(); const email = emailArg.trim().toLowerCase(); const result = q.deleteInvite.run(email); console.log(result.changes > 0 ? `Removed ${email}` : `No invite for ${email}`); break; } case 'list': { const rows = q.listInvites.all(); if (rows.length === 0) { console.log('(no invites)'); } else { for (const r of rows) { const d = new Date(r.invited_at).toISOString().slice(0, 10); const name = r.first_name ? ` [${r.first_name}]` : ''; const by = r.invited_by ? ` (by ${r.invited_by})` : ''; const admin = r.is_admin ? ' *ADMIN*' : ''; console.log(` ${d} ${r.email}${name}${by}${admin}`); } console.log(`\n${rows.length} invite${rows.length === 1 ? '' : 's'} total.`); } break; } case 'admin': { if (sub === 'add' || sub === 'remove') { const emailArg = arg3; if (!emailArg || !EMAIL_RE.test(emailArg)) help(); const email = emailArg.trim().toLowerCase(); const invite = q.getInvite.get(email); if (!invite) { console.log(`No invite for ${email} — invite them first with: invite add ${email}`); process.exit(1); } q.setInviteAdmin.run(sub === 'add' ? 1 : 0, email); console.log(sub === 'add' ? `Granted admin to ${email}` : `Revoked admin from ${email}`); } else if (sub === 'list') { const rows = q.listInvites.all().filter((r) => r.is_admin); if (rows.length === 0) { console.log('(no admins)'); } else { for (const r of rows) { const name = r.first_name ? ` [${r.first_name}]` : ''; console.log(` ${r.email}${name}`); } console.log(`\n${rows.length} admin${rows.length === 1 ? '' : 's'} total.`); } } else { help(); } break; } default: help(); }