import type { Argv, ArgumentsCamelCase } from "yargs" import { CLICommand } from "../cli/CLICommand" import { parseTodoFile } from "../file" import { BugzillaClient } from "../bugzilla/client" import { loadConfig } from "../bugzilla/config" interface ProductState { name: string exists: boolean components: { name: string exists: boolean }[] } export class InitCommand extends CLICommand { readonly name = "init" readonly help = "Initialize Bugzilla products and components" readonly description = "Check and optionally create Bugzilla products/components from TODO tracker config" addArguments(yargs: Argv): Argv { return yargs .option("dry-run", { type: "boolean", default: false, description: "Show what would be created without creating", }) .option("confirm", { type: "boolean", default: false, description: "Actually create missing products/components (required for writes)", }) .option("assignee", { type: "string", description: "Default assignee email for new components", }) } async execute(args: ArgumentsCamelCase): Promise { const todo = await parseTodoFile() if (!todo.bugzilla) { console.error("No application/bugzilla part found in TODO file") return 1 } const config = loadConfig() const client = new BugzillaClient(config) const dryRun = args.dryRun as boolean const confirm = args.confirm as boolean const assignee = args.assignee as string | undefined // Collect unique products and their components from the mappings const productMap = new Map>() for (const m of todo.bugzilla.mappings) { if (!productMap.has(m.product)) productMap.set(m.product, new Set()) productMap.get(m.product)!.add(m.component) } // Check what exists on the server const states: ProductState[] = [] const existingProducts = await client.getAccessibleProducts() const existingByName = new Map(existingProducts.map(p => [p.name, p])) for (const [productName, componentNames] of productMap) { const existing = existingByName.get(productName) const existingComponents = new Set( existing?.components?.map(c => c.name) ?? [] ) states.push({ name: productName, exists: !!existing, components: [...componentNames].map(name => ({ name, exists: existingComponents.has(name), })), }) } // Report let allOk = true for (const product of states) { const pStatus = product.exists ? "ok" : "MISSING" console.log(`Product: ${product.name} [${pStatus}]`) for (const comp of product.components) { const cStatus = comp.exists ? "ok" : "MISSING" console.log(` Component: ${comp.name} [${cStatus}]`) if (!comp.exists) allOk = false } if (!product.exists) allOk = false } if (allOk) { console.log("\nAll products and components exist. Nothing to do.") return 0 } if (dryRun) { console.log("\n[dry-run] Would create the MISSING items above.") return 0 } if (!confirm) { console.log("\nMissing items found. Run with --confirm to create them.") return 1 } // Need assignee for component creation const needsComponents = states.some(p => p.components.some(c => !c.exists) ) if (needsComponents && !assignee) { console.error("\n--assignee is required when creating components") return 1 } // Create missing products and components let created = 0 for (const product of states) { if (!product.exists) { try { const id = await client.createProduct({ name: product.name, description: product.name, version: "unspecified", }) console.log(`Created product #${id}: ${product.name}`) created++ } catch (err: any) { console.error(`Error creating product "${product.name}": ${err.message}`) continue } } for (const comp of product.components) { if (!comp.exists) { try { const id = await client.createComponent({ product: product.name, name: comp.name, description: comp.name, default_assignee: assignee!, }) console.log(`Created component #${id}: ${comp.name} (in ${product.name})`) created++ } catch (err: any) { console.error(`Error creating component "${comp.name}": ${err.message}`) } } } } console.log(`\nInit complete: ${created} items created`) return 0 } }