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 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 19:09:09 +08:00
parent cc77211969
commit 3031c88148
3 changed files with 102 additions and 8 deletions

View File

@@ -6,4 +6,10 @@ export const app = buildApplication(rootRouteMap, {
versionInfo: { versionInfo: {
currentVersion: "0.0.0", currentVersion: "0.0.0",
}, },
scanner: {
caseStyle: "allow-kebab-for-camel",
},
documentation: {
caseStyle: "convert-camel-to-kebab",
},
}); });

View File

@@ -6,6 +6,7 @@ import { writeConfig } from "../utils/config.js";
interface BootstrapFlags { interface BootstrapFlags {
email: string; email: string;
password: string; password: string;
dangerouslyOverwriteExisting: boolean;
} }
async function bootstrap( async function bootstrap(
@@ -22,6 +23,7 @@ async function bootstrap(
const result = await executeBootstrap(db, { const result = await executeBootstrap(db, {
email: flags.email, email: flags.email,
password: flags.password, password: flags.password,
dangerouslyOverwriteExisting: flags.dangerouslyOverwriteExisting,
}); });
console.log(`Created superuser: ${result.user.email}`); console.log(`Created superuser: ${result.user.email}`);
@@ -62,6 +64,11 @@ export const bootstrapCommand = buildCommand({
parse: String, parse: String,
brief: "Password for the superuser", brief: "Password for the superuser",
}, },
dangerouslyOverwriteExisting: {
kind: "boolean",
brief: "Delete existing user and reviq org if they exist",
default: false,
},
}, },
}, },
docs: { docs: {

View File

@@ -26,6 +26,8 @@ export interface BootstrapInput {
tokenName?: string; tokenName?: string;
/** Optional token expiration in days (defaults to 365) */ /** Optional token expiration in days (defaults to 365) */
tokenExpirationDays?: number; 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", orgDisplayName = "RevIQ",
tokenName = "CLI bootstrap token", tokenName = "CLI bootstrap token",
tokenExpirationDays = 365, tokenExpirationDays = 365,
dangerouslyOverwriteExisting = false,
} = input; } = input;
// Validate password length // Validate password length
@@ -84,15 +87,93 @@ export const executeBootstrap = async (
const normalizedEmail = email.toLowerCase(); const normalizedEmail = email.toLowerCase();
// Check if user already exists // Handle overwrite mode - delete existing user and org
const existing = await trx if (dangerouslyOverwriteExisting) {
.selectFrom("users") // Delete existing user and related records
.where("email", "=", normalizedEmail) const existingUser = await trx
.select("id") .selectFrom("users")
.executeTakeFirst(); .where("email", "=", normalizedEmail)
.select("id")
.executeTakeFirst();
if (existing) { if (existingUser) {
throw new Error(`User with email ${email} already exists`); // 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 // Hash the password