#!/usr/bin/env npx tsx /** * CLI for adding bands/artists directly to the DB (no server required). * * Usage: * npx tsx scripts/add.ts band --name "Name" [--slug x] [--area X] [--status active|hiatus|disbanded] [--description X] [--message X] * npx tsx scripts/add.ts artist --name "Name" [--slug x] [--message X] * npx tsx scripts/add.ts import --file data.json * * JSON import format (data.json): * { * "artists": [{ "name": "...", "links": [{ "label": "Twitter", "url": "..." }] }], * "bands": [{ * "name": "...", "area": "...", "status": "active", * "members": [{ "name": "...", "role": "Vocal", "since": "2020", "until": "", "note": "" }], * "links": [{ "label": "Twitter", "url": "..." }] * }] * } * * Artist references in members can use { "id": "" } or { "name": "..." } (looked up by name). * Artists listed under "artists" are created first so band references by name work. * Run from the project root so whois.db is found automatically. */ import { readFileSync } from "fs"; import { createArtist, createBand, listArtists, toSlug, type MemberInput } from "../app/lib/db.server"; type LinkInput = { label: string; url: string }; type MemberRef = { id?: string; name?: string; role?: string | null; since?: string; until?: string; note?: string }; function parseFlags(argv: string[]): Record { const flags: Record = {}; for (let i = 0; i < argv.length; i++) { if (argv[i].startsWith("--")) { const key = argv[i].slice(2); flags[key] = argv[i + 1] ?? "true"; i++; } } return flags; } const [, , command, ...rest] = process.argv; if (!command || command === "--help" || command === "-h") { console.log(`Usage: npx tsx scripts/add.ts band --name "Name" [--slug x] [--area X] [--status active|hiatus|disbanded] [--description X] [--message X] npx tsx scripts/add.ts artist --name "Name" [--slug x] [--message X] npx tsx scripts/add.ts import --file data.json`); process.exit(0); } if (command === "artist") { const flags = parseFlags(rest); const name = flags.name?.trim(); if (!name) { console.error("Error: --name is required"); process.exit(1); } const slug = flags.slug?.trim() || toSlug(name); const id = crypto.randomUUID(); const artist = createArtist({ id, slug, name, links: flags.links ? (JSON.parse(flags.links) as LinkInput[]) : [], message: flags.message || "CLI import", ip_address: "cli", }); console.log(`Created artist: ${artist.name}`); console.log(` id: ${artist.id}`); console.log(` url: /artists/of/${artist.id}`); } else if (command === "band") { const flags = parseFlags(rest); const name = flags.name?.trim(); if (!name) { console.error("Error: --name is required"); process.exit(1); } const slug = flags.slug?.trim() || toSlug(name); const id = crypto.randomUUID(); try { const band = createBand({ id, slug, name, area: flags.area || null, description: flags.description || null, status: flags.status || "active", links: flags.links ? (JSON.parse(flags.links) as LinkInput[]) : [], members: flags.members ? (JSON.parse(flags.members) as MemberInput[]) : [], message: flags.message || "CLI import", ip_address: "cli", }); console.log(`Created band: ${band.name}`); console.log(` id: ${band.id}`); console.log(` url: /bands/of/${band.id}`); } catch (e) { if (e instanceof Error && e.message.includes("UNIQUE constraint failed: bands.slug")) { console.error(`Error: slug "${slug}" is already in use`); process.exit(1); } throw e; } } else if (command === "import") { const flags = parseFlags(rest); if (!flags.file) { console.error("Error: --file is required"); process.exit(1); } const data = JSON.parse(readFileSync(flags.file, "utf-8")) as { artists?: { name: string; slug?: string; links?: LinkInput[]; message?: string }[]; bands?: { name: string; slug?: string; area?: string; description?: string; status?: string; links?: LinkInput[]; members?: MemberRef[]; message?: string }[]; }; // Build name→id map from existing artists, then create new ones const nameToId = new Map(listArtists().map((a) => [a.name, a.id])); for (const a of data.artists ?? []) { const name = a.name?.trim(); if (!name) continue; if (nameToId.has(name)) { console.log(`Skip existing artist: ${name}`); continue; } const id = crypto.randomUUID(); createArtist({ id, slug: a.slug?.trim() || toSlug(name), name, links: a.links || [], message: a.message || "JSON import", ip_address: "cli", }); nameToId.set(name, id); console.log(`Created artist: ${name} (${id})`); } for (const b of data.bands ?? []) { const name = b.name?.trim(); if (!name) continue; const id = crypto.randomUUID(); const members = (b.members ?? []) .map((mr) => { const artistId = mr.id ?? (mr.name ? nameToId.get(mr.name) : undefined); if (!artistId) { console.warn(` Warning: artist not found: "${mr.name}"`); return null; } return { artist_id: artistId, role: mr.role ?? null, since: mr.since ?? "", until: mr.until ?? "", note: mr.note ?? "" }; }) .filter((x): x is MemberInput => x !== null); try { createBand({ id, slug: b.slug?.trim() || toSlug(name), name, area: b.area || null, description: b.description || null, status: b.status || "active", links: b.links || [], members, message: b.message || "JSON import", ip_address: "cli", }); console.log(`Created band: ${name} (${id})`); } catch (e) { if (e instanceof Error && e.message.includes("UNIQUE constraint failed: bands.slug")) { console.error(`Skip band "${name}": slug already in use`); } else { throw e; } } } } else { console.error(`Unknown command: "${command}". Use band, artist, or import.`); process.exit(1); }