From 3031c881481247d6360bb3666e09449e405ee317 Mon Sep 17 00:00:00 2001 From: RevIQ Date: Fri, 9 Jan 2026 19:09:09 +0800 Subject: [PATCH] Add --dangerously-overwrite-existing flag to bootstrap command Allows re-running bootstrap to delete and recreate the superuser and reviq org. Deletes all related records (tokens, sessions, passkeys, etc.) before recreating. Also configures stricli to use kebab-case for CLI flags globally. Co-Authored-By: Claude Opus 4.5 --- apps/cli/src/app.ts | 6 ++ apps/cli/src/routes/bootstrap.ts | 7 ++ packages/db/src/helpers/execute-bootstrap.ts | 97 ++++++++++++++++++-- 3 files changed, 102 insertions(+), 8 deletions(-) diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts index 747763f..1c49f97 100644 --- a/apps/cli/src/app.ts +++ b/apps/cli/src/app.ts @@ -6,4 +6,10 @@ export const app = buildApplication(rootRouteMap, { versionInfo: { currentVersion: "0.0.0", }, + scanner: { + caseStyle: "allow-kebab-for-camel", + }, + documentation: { + caseStyle: "convert-camel-to-kebab", + }, }); diff --git a/apps/cli/src/routes/bootstrap.ts b/apps/cli/src/routes/bootstrap.ts index 27be6bb..5dd38cf 100644 --- a/apps/cli/src/routes/bootstrap.ts +++ b/apps/cli/src/routes/bootstrap.ts @@ -6,6 +6,7 @@ import { writeConfig } from "../utils/config.js"; interface BootstrapFlags { email: string; password: string; + dangerouslyOverwriteExisting: boolean; } async function bootstrap( @@ -22,6 +23,7 @@ async function bootstrap( const result = await executeBootstrap(db, { email: flags.email, password: flags.password, + dangerouslyOverwriteExisting: flags.dangerouslyOverwriteExisting, }); console.log(`Created superuser: ${result.user.email}`); @@ -62,6 +64,11 @@ export const bootstrapCommand = buildCommand({ parse: String, brief: "Password for the superuser", }, + dangerouslyOverwriteExisting: { + kind: "boolean", + brief: "Delete existing user and reviq org if they exist", + default: false, + }, }, }, docs: { diff --git a/packages/db/src/helpers/execute-bootstrap.ts b/packages/db/src/helpers/execute-bootstrap.ts index 0ddf36f..9957e09 100644 --- a/packages/db/src/helpers/execute-bootstrap.ts +++ b/packages/db/src/helpers/execute-bootstrap.ts @@ -26,6 +26,8 @@ export interface BootstrapInput { tokenName?: string; /** Optional token expiration in days (defaults to 365) */ tokenExpirationDays?: number; + /** Delete existing user and org if they exist (defaults to false) */ + dangerouslyOverwriteExisting?: boolean; } /** @@ -70,6 +72,7 @@ export const executeBootstrap = async ( orgDisplayName = "RevIQ", tokenName = "CLI bootstrap token", tokenExpirationDays = 365, + dangerouslyOverwriteExisting = false, } = input; // Validate password length @@ -84,15 +87,93 @@ export const executeBootstrap = async ( const normalizedEmail = email.toLowerCase(); - // Check if user already exists - const existing = await trx - .selectFrom("users") - .where("email", "=", normalizedEmail) - .select("id") - .executeTakeFirst(); + // Handle overwrite mode - delete existing user and org + if (dangerouslyOverwriteExisting) { + // Delete existing user and related records + const existingUser = await trx + .selectFrom("users") + .where("email", "=", normalizedEmail) + .select("id") + .executeTakeFirst(); - if (existing) { - throw new Error(`User with email ${email} already exists`); + if (existingUser) { + // Delete all user-related records (FK constraints) + await trx + .deleteFrom("api_tokens") + .where("user_id", "=", existingUser.id) + .execute(); + await trx + .deleteFrom("email_verifications") + .where("user_id", "=", existingUser.id) + .execute(); + await trx + .deleteFrom("login_requests") + .where("user_id", "=", existingUser.id) + .execute(); + await trx + .deleteFrom("passkeys") + .where("user_id", "=", existingUser.id) + .execute(); + await trx + .deleteFrom("password_resets") + .where("user_id", "=", existingUser.id) + .execute(); + await trx + .deleteFrom("sessions") + .where("user_id", "=", existingUser.id) + .execute(); + await trx + .deleteFrom("user_devices") + .where("user_id", "=", existingUser.id) + .execute(); + await trx + .deleteFrom("org_members") + .where("user_id", "=", existingUser.id) + .execute(); + // Delete invites created by this user + await trx + .deleteFrom("org_invites") + .where("invited_by", "=", existingUser.id) + .execute(); + // Delete the user + await trx.deleteFrom("users").where("id", "=", existingUser.id).execute(); + } + + // Delete existing org and related records + const existingOrg = await trx + .selectFrom("orgs") + .where("slug", "=", orgSlug) + .select("id") + .executeTakeFirst(); + + if (existingOrg) { + // Delete all org-related records (FK constraints) + await trx + .deleteFrom("org_invites") + .where("org_id", "=", existingOrg.id) + .execute(); + await trx + .deleteFrom("org_members") + .where("org_id", "=", existingOrg.id) + .execute(); + await trx + .deleteFrom("org_sites") + .where("org_id", "=", existingOrg.id) + .execute(); + // Delete the org + await trx.deleteFrom("orgs").where("id", "=", existingOrg.id).execute(); + } + } else { + // Check if user already exists (normal mode) + const existing = await trx + .selectFrom("users") + .where("email", "=", normalizedEmail) + .select("id") + .executeTakeFirst(); + + if (existing) { + throw new Error(`User with email ${email} already exists`); + } } // Hash the password