UNPKG

@beepshq/1dollar

Version:

On-Call for $1/mo

1,227 lines (1,211 loc) 38 kB
#!/usr/bin/env node // src/cli/log.ts import chalk from "chalk"; var log = { info: (msg) => console.log(chalk.blue(msg)), success: (msg) => console.log(chalk.green(msg)), warning: (msg) => console.log(chalk.yellow(msg)), error: (msg) => console.log(chalk.red(msg)), title: (msg) => console.log(chalk.bold.cyan(msg)), code: (msg) => console.log(chalk.bgBlack.white(msg)) }; // src/cli/auth.ts import chalk2 from "chalk"; import { input } from "@inquirer/prompts"; import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync } from "node:fs"; import { join as join2 } from "node:path"; import { homedir as homedir2 } from "os"; // src/cli/auth-client.ts import { createAuthClient } from "better-auth/client"; import { emailOTPClient, magicLinkClient } from "better-auth/client/plugins"; import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "os"; // src/config.ts function getBaseUrl({ prodUrl = false } = {}) { if (prodUrl) return "https://1dollar.fly.dev"; if (process.env.PLAYWRIGHT) return "http://localhost:4322"; if (process.env.BASE_URL) return process.env.BASE_URL; return prodUrl ? "https://1dollar.fly.dev" : "http://localhost:4321"; } var DEFAULT_ROUTING_PROMPT = `You are a routing system that determines which on-call rotation should handle an alert. Consider the alert's title, description, source, and any relevant information in the payload. Choose the most appropriate rotation based on the alert content and rotation names.`; var verbose = false; // src/cli/auth-client.ts var AUTH_FILE = join(homedir(), ".beeps", "auth.json"); var getTokenFromFile = () => { if (!existsSync(AUTH_FILE)) { return ""; } try { const data = JSON.parse(readFileSync(AUTH_FILE, "utf-8")); return data.token || ""; } catch (error) { console.error("Failed to read auth token from file:", error); return ""; } }; var cliAuthClient = createAuthClient({ baseURL: getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT }), plugins: [emailOTPClient(), magicLinkClient()], fetchOptions: { auth: { type: "Bearer", token: getTokenFromFile // Use our custom function to get the token } } }); // src/cli/auth.ts var STORAGE_DIR = join2(homedir2(), ".beeps"); var AUTH_FILE2 = join2(STORAGE_DIR, "auth.json"); var AuthStore = class { _token = ""; _user = null; constructor() { if (!existsSync2(STORAGE_DIR)) { mkdirSync(STORAGE_DIR, { recursive: true }); } if (existsSync2(AUTH_FILE2)) { try { const data = JSON.parse(readFileSync2(AUTH_FILE2, "utf-8")); if (data.token && data.user) { this._token = data.token; this._user = data.user; } } catch (error) { console.error("Failed to load auth state:", error); } } } get token() { return this._token; } get user() { return this._user; } get isValid() { return !!this._token && !!this._user; } get email() { return this._user?.email; } save(token, user) { this._token = token; this._user = user; try { writeFileSync( AUTH_FILE2, JSON.stringify( { token, user }, null, 2 ) ); } catch (error) { console.error("Failed to save auth state:", error); } } clear() { this._token = ""; this._user = null; try { if (existsSync2(AUTH_FILE2)) { writeFileSync( AUTH_FILE2, JSON.stringify( { token: "", user: null }, null ) ); } } catch (error) { console.error("Failed to clear auth state:", error); } } }; var authStore = new AuthStore(); async function handleLogin() { log.title("\nWelcome!"); const email = await input({ message: "Enter your email:", validate: (value) => { if (!value.includes("@")) { return "Please enter a valid email address"; } return true; } }); try { log.info(` Sending login code to ${chalk2.bold(email)}...`); const sendResponse = await cliAuthClient.emailOtp.sendVerificationOtp({ email, type: "sign-in" }); verbose && console.log("Send response:", JSON.stringify(sendResponse, null, 2)); const { error: sendError } = sendResponse; if (sendError) { throw new Error(`Failed to send verification code: ${sendError.message}`); } log.success(` A login code has been sent to ${chalk2.bold(email)}`); const code = await input({ message: "Enter the login code:" }); const { data, error } = await cliAuthClient.signIn.emailOtp({ email, otp: code }); if (error || !data) { throw new Error( `Failed to verify login code: ${error?.message || "Unknown error"}` ); } const token = data.token || ""; const user = data.user || null; authStore.save(token, user); log.success("\n\u2728 Successfully logged in!"); return { token, userId: user?.id || "", email: user?.email || "" }; } catch (error) { if (error instanceof Error) { log.error(` Login failed: ${error.message}`); console.error("Full error:", error); } else { log.error("\nLogin failed: Unknown error"); console.error("Full error:", error); } process.exit(1); } } async function ensureLoggedIn() { try { if (!authStore.isValid) { log.warning("You need to login first"); return handleLogin(); } try { const { data, error } = await cliAuthClient.getSession(); if (error || !data) { throw new Error("Session invalid"); } if (data.user && data.user.id !== authStore.user?.id) { authStore.save(authStore.token, data.user); } } catch (error) { log.warning("Your session has expired. Please login again."); authStore.clear(); return handleLogin(); } return { token: authStore.token, userId: authStore.user?.id || "", email: authStore.user?.email || "" }; } catch (error) { log.warning("Authentication error. Please login again."); return handleLogin(); } } // src/cli/alerts.ts import Table from "cli-table3"; import chalk3 from "chalk"; async function displayAlerts(alertsData) { if (!alertsData.ok) { log.error(alertsData.error); process.exit(1); } try { if (!alertsData.alerts || !alertsData.alerts.length) { log.info("\nNo alerts found"); return; } const table = new Table({ head: ["Date", "Title", "Source", "Rotation", "Payload"].map( (h) => chalk3.cyan(h) ), colWidths: [12, 35, 15, 20, 40], wordWrap: true }); log.info("\nCurrent routing prompt:"); log.info(chalk3.dim(alertsData.routingPrompt)); log.info(""); for (const alert of alertsData.alerts) { const date = new Date(alert.created || "").toISOString().split("T")[0]; const payload = JSON.stringify(alert.payload || {}); const truncatedPayload = payload.length > 37 ? payload.slice(0, 37) + "..." : payload; table.push([ date, alert.title, alert.source || "-", alert.rotation?.name || "-", truncatedPayload ]); } console.log(table.toString()); log.info(` Total alerts: ${alertsData.alerts.length}`); } catch (error) { log.error("Failed to display alerts"); console.error(error); process.exit(1); } } async function fetchAlerts(token) { try { const alertsResponse = await fetch( `${getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT })}/api/alerts`, { headers: { Authorization: `Bearer ${token}` } } ); if (!alertsResponse.ok) { throw new Error(`Failed to fetch alerts: ${alertsResponse.statusText}`); } const alertsData = await alertsResponse.json(); return displayAlerts(alertsData); } catch (error) { log.error("Failed to fetch alerts"); if (error instanceof Error) { console.error(error.message); } else { console.error(error); } process.exit(1); } } // src/cli/configurations.ts import chalk4 from "chalk"; import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs"; // src/cli/dev.ts var isDev = !!process.env.BEEPS_DEVELOPMENT; var verbose2 = false; // src/cli/configurations.ts import { writeFile } from "node:fs/promises"; // src/domain/scheduler.ts var debug = false; function sortShiftables(shiftables, sortOrder) { const sortedShiftables = [...shiftables].sort((a, b) => { if (sortOrder.type === "custom") { const customResult = sortOrder.fn(a, b); if (customResult === 0) { return a.id.localeCompare(b.id); } return customResult; } return a.id.localeCompare(b.id); }); for (const shiftable of sortedShiftables) { if (shiftable.forcedDates) { shiftable.forcedDates.sort((a, b) => a.localeCompare(b)); } if (shiftable.excludedDates) { shiftable.excludedDates.sort((a, b) => a.localeCompare(b)); } } return sortedShiftables; } function formatDate(date) { return date.toISOString().split("T")[0]; } function validateDate(date, fieldName) { if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { throw new Error( `invalid ${fieldName} format: ${date}, expected YYYY-MM-DD` ); } if (isNaN(new Date(date).getTime())) { throw new Error(`invalid ${fieldName}: ${date}`); } } function validateShiftable(shiftable, index) { if (typeof shiftable.id !== "string" || shiftable.id.trim() === "") { throw new Error( `shiftable at index ${index} has an invalid id: ${shiftable.id}` ); } shiftable.forcedDates?.forEach((date) => validateDate(date, "forcedDate")); shiftable.excludedDates?.forEach( (date) => validateDate(date, "excludedDate") ); const conflicts = shiftable.forcedDates?.filter( (date) => shiftable.excludedDates?.includes(date) ); if (conflicts?.length) { throw new Error( `shiftable ${shiftable.id} has dates that are both forced and excluded: ${conflicts.join(", ")}` ); } } function renderShifts(shiftables, options, viewOptions) { debug && console.log("\n=== Starting renderShifts ==="); debug && console.log("Input:", { shiftables, options, viewOptions }); if (!Array.isArray(shiftables) || shiftables.length === 0) { throw new Error("shiftables must be a non-empty array"); } shiftables.forEach(validateShiftable); validateDate(options.startDate, "startDate"); let viewFromDate; let viewToDate; if ("fromDate" in viewOptions && "toDate" in viewOptions) { viewFromDate = new Date(viewOptions.fromDate); viewToDate = new Date(viewOptions.toDate); debug && console.log("View range:", { viewFromDate, viewToDate }); if (isNaN(viewFromDate.getTime())) { throw new Error(`invalid fromDate: ${viewOptions.fromDate}`); } if (isNaN(viewToDate.getTime())) { throw new Error(`invalid toDate: ${viewOptions.toDate}`); } if (viewFromDate >= viewToDate) { throw new Error( `fromDate (${viewOptions.fromDate}) must be before toDate (${viewOptions.toDate})` ); } if (viewFromDate < new Date(options.startDate)) { throw new Error( `fromDate (${viewOptions.fromDate}) must be after or equal to startDate (${options.startDate})` ); } } else if ("fromDate" in viewOptions && "shiftsCount" in viewOptions) { const fromDate = new Date(viewOptions.fromDate); if (isNaN(fromDate.getTime())) { throw new Error(`invalid fromDate: ${viewOptions.fromDate}`); } if (fromDate < new Date(options.startDate)) { throw new Error( `fromDate (${viewOptions.fromDate}) must be after or equal to startDate (${options.startDate})` ); } if (typeof viewOptions.shiftsCount !== "number" || viewOptions.shiftsCount <= 0) { throw new Error( `shiftsCount must be a positive number, got: ${viewOptions.shiftsCount}` ); } viewFromDate = fromDate; const dayMs = 24 * 60 * 60 * 1e3; const weekMs = 7 * dayMs; const intervalMs = options.rotationType === "weekly" ? weekMs : dayMs; viewToDate = new Date( viewFromDate.getTime() + viewOptions.shiftsCount * intervalMs ); debug && console.log("View range:", { viewFromDate, viewToDate }); } else { throw new Error( "invalid viewOptions: must contain either 'fromDate' and 'toDate' or 'fromDate' and 'shiftsCount'" ); } const sortedShiftables = sortShiftables(shiftables, options.sortOrder); debug && console.log("Sorted shiftables:", sortedShiftables); const shifts = []; let currentDate = new Date(options.startDate); debug && console.log("Starting from date:", currentDate); function getShiftIndex(date) { const startDate = new Date(options.startDate); const currentDate2 = new Date(date); startDate.setUTCHours(0, 0, 0, 0); currentDate2.setUTCHours(0, 0, 0, 0); const dayMs = 24 * 60 * 60 * 1e3; const diffDays = Math.floor( (currentDate2.getTime() - startDate.getTime()) / dayMs ); debug && console.log("Shift index calculation:", { startDate, currentDate: currentDate2, diffDays, weekNumber: Math.floor(diffDays / 7), isWeekly: options.rotationType === "weekly" }); if (options.rotationType === "weekly") { return Math.floor(diffDays / 7); } return diffDays; } while (currentDate < viewToDate) { debug && console.log("\n=== Processing date:", currentDate, "==="); const shiftDuration = options.rotationType === "weekly" ? 7 : 1; const shiftEndDate = new Date(currentDate); shiftEndDate.setDate(shiftEndDate.getDate() + shiftDuration); const currentDateString = formatDate(currentDate); const shiftIndex = getShiftIndex(currentDate); debug && console.log("Initial shift index:", shiftIndex); let assignedShiftable = sortedShiftables[shiftIndex % sortedShiftables.length]; debug && console.log("Initial assigned shiftable:", assignedShiftable); const forcedShiftable = sortedShiftables.find( (s) => s.forcedDates?.includes(currentDateString) ); if (forcedShiftable) { debug && console.log("Found forced shiftable:", forcedShiftable); assignedShiftable = forcedShiftable; } else { let checkedCount = 0; let foundNonConsecutive = false; debug && console.log( "Trying to find non-consecutive and non-excluded shiftable..." ); while (checkedCount < sortedShiftables.length && !foundNonConsecutive) { const candidateIndex = (shiftIndex + checkedCount) % sortedShiftables.length; assignedShiftable = sortedShiftables[candidateIndex]; debug && console.log("Checking candidate:", { checkedCount, candidateIndex, candidateId: assignedShiftable?.id, isExcluded: assignedShiftable?.excludedDates?.includes(currentDateString), isConsecutive: shifts.length > 0 && shifts[shifts.length - 1]?.shiftableId === assignedShiftable?.id }); checkedCount++; if (assignedShiftable?.excludedDates?.includes(currentDateString)) { debug && console.log("Skipping excluded shiftable:", assignedShiftable.id); continue; } if (shifts.length > 0 && shifts[shifts.length - 1]?.shiftableId === assignedShiftable?.id) { debug && console.log( "Skipping consecutive shiftable:", assignedShiftable?.id ); continue; } foundNonConsecutive = true; } if (!foundNonConsecutive) { debug && console.log( "No non-consecutive shiftable found, trying to find non-excluded shiftable..." ); checkedCount = 0; let foundNonExcluded = false; while (checkedCount < sortedShiftables.length && !foundNonExcluded) { const candidateIndex = (shiftIndex + checkedCount) % sortedShiftables.length; assignedShiftable = sortedShiftables[candidateIndex]; debug && console.log("Checking candidate (allowing consecutive):", { checkedCount, candidateIndex, candidateId: assignedShiftable?.id, isExcluded: assignedShiftable?.excludedDates?.includes(currentDateString) }); checkedCount++; if (!assignedShiftable?.excludedDates?.includes(currentDateString)) { foundNonExcluded = true; } } if (!foundNonExcluded) { throw new Error( `no available shiftable found for date ${currentDateString}` ); } } } if (!assignedShiftable) { debug && console.log("no assigned shiftable found"); continue; } debug && console.log("Final assigned shiftable:", assignedShiftable); shifts.push({ shiftableId: assignedShiftable.id, startDate: currentDateString, endDate: formatDate(shiftEndDate) }); currentDate = shiftEndDate; } const filteredShifts = shifts.filter((shift) => new Date(shift.endDate) > viewFromDate).slice( 0, "shiftsCount" in viewOptions ? viewOptions.shiftsCount : void 0 ); debug && console.log("\n=== Final shifts ==="); debug && console.log(JSON.stringify(filteredShifts, null, 2)); return filteredShifts; } // src/domain/beeps-json-schema.ts import { z } from "zod"; var RotationSchema = z.object({ name: z.string(), members: z.array(z.string()), frequency: z.enum(["daily", "weekly"]), startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format") }); var BeepsConfigurationSchema = z.object({ routingPrompt: z.string().optional(), rotations: z.array(RotationSchema) }); // src/cli/configurations.ts var CONFIG_FILE_PATH = "beeps.json"; function readConfigurationFile() { try { if (!existsSync3(CONFIG_FILE_PATH)) { return null; } const data = readFileSync3(CONFIG_FILE_PATH, "utf-8"); const parsed = JSON.parse(data); if (parsed.members && !parsed.rotations) { return BeepsConfigurationSchema.parse({ rotations: [ { name: "default", members: parsed.members, frequency: parsed.frequency, startDate: parsed.startDate } ] }); } return BeepsConfigurationSchema.parse(parsed); } catch (error) { if (error instanceof Error) { log.error(`Failed to read configuration file: ${error.message}`); verbose2 && console.error("Detailed error:", error); } else { log.error("Failed to read configuration file"); verbose2 && console.error("Detailed error:", error); } return null; } } async function hasRotationsConfigFile() { return existsSync3(CONFIG_FILE_PATH); } async function writeDefaultRotationsConfigFile({ emails }) { try { await writeFile( CONFIG_FILE_PATH, JSON.stringify( { routingPrompt: DEFAULT_ROUTING_PROMPT, rotations: [ { name: "default", members: emails, startDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0], frequency: "daily" } ] }, null, 2 ) ); } catch (error) { if (error instanceof Error) { log.error(`Failed to write configuration file: ${error.message}`); verbose2 && console.error("Detailed error:", error); } else { log.error("Failed to write configuration file"); verbose2 && console.error("Detailed error:", error); } throw new Error("Failed to write rotation configuration"); } } async function pushConfiguration(token, userId, config) { try { const response = await fetch( `${getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT })}/api/cli/push-configuration`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify(config) } ); if (!response.ok) { if (response.status === 400) { const { error } = await response.json(); throw new Error(`Invalid configuration: ${JSON.stringify(error)}`); } throw new Error(`Failed to push configuration: ${response.statusText}`); } } catch (error) { if (error instanceof Error) { log.error(`Failed to push configuration: ${error.message}`); verbose2 && console.error("Detailed error:", error); } else { log.error("Failed to push configuration"); verbose2 && console.error("Detailed error:", error); } throw error; } } async function renderRotation({ pendingRotation, activeRotation }) { const Table2 = (await import("cli-table3")).default; async function renderRotationSection(rotation, isActive) { if (!rotation) return; const stateEmoji = isActive ? "\u{1F7E2}" : "\u23F3"; const stateColor = isActive ? chalk4.green : chalk4.yellow; const stateText = isActive ? "Active" : "Pending"; log.info( `${stateEmoji} ${stateColor(`${stateText}`)} (started ${new Date(rotation.startDate).toISOString().split("T")[0]})` ); const table = new Table2({ head: ["Engineer", "Status", "Notifications"], style: { head: ["blue"] } }); rotation.members?.forEach((member) => { const notifications = [ // Show checkmark for verified email member.emailVerified ? "\u2713 Email" : "", // Keep phone logic intact member.phoneEnabled ? member.phoneVerified ? "\u2713 SMS" : "\u26A0 SMS" : "" ].filter(Boolean).join(", ") || "None"; let status; if (member.emailVerified || member.userId) { status = chalk4.green("Active"); } else if (member.status === "invited") { status = chalk4.yellow("Invited"); } else if (member.status === "pending") { status = chalk4.yellow("Pending"); } else if (member.status === "declined") { status = chalk4.red("Declined"); } else { status = chalk4.gray(member.status); } const displayNotifications = member.status === "invited" && !member.emailVerified && !member.userId ? "X" : notifications; table.push([chalk4.white(member.email), status, displayNotifications]); }); rotation.invitations?.filter((invite) => invite.status !== "active").forEach((invite) => { table.push([ chalk4.white(invite.email), chalk4.yellow(invite.status), "Pending" ]); }); await renderShiftPreview(rotation, table); if (table.length > 0) { console.log(table.toString()); } else { log.info("No members or pending invitations"); } } if (activeRotation) { await renderRotationSection(activeRotation, true); } if (pendingRotation) { if (activeRotation) { log.info( "\n" + chalk4.yellow( "\u2193 Will replace active rotation when all invitations are accepted \u2193" ) ); } await renderRotationSection(pendingRotation, false); } } async function renderShiftPreview(rotation, table) { if (!rotation.members?.length) { return; } const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]; const shiftables = rotation.members.map((member) => ({ id: member.email // Use email as ID since it's displayed in the table })); const startDate = new Date(rotation.startDate).toISOString().split("T")[0]; const shifts = renderShifts( shiftables, { rotationType: rotation.frequency, sortOrder: { type: "id" }, startDate }, { fromDate: today, shiftsCount: 4 } ); table.push([chalk4.blue("Upcoming Shifts"), "", ""]); shifts.forEach((shift) => { table.push([ chalk4.dim(`${shift.startDate} \u2192 ${shift.endDate}`), chalk4.dim(shift.shiftableId), chalk4.dim("Scheduled") ]); }); } async function displayRotations(rotationsData) { try { if (rotationsData.active.length > 0) { log.info("\nActive Rotations:"); for (const rotation of rotationsData.active) { log.info(` ${chalk4.blue(`Rotation: ${rotation.name}`)}`); await renderRotation({ activeRotation: rotation }); } } if (rotationsData.pending.length > 0) { log.info("\nPending Rotations:"); for (const rotation of rotationsData.pending) { log.info(` ${chalk4.blue(`Rotation: ${rotation.name}`)}`); await renderRotation({ pendingRotation: rotation }); } } if (!rotationsData.active.length && !rotationsData.pending.length) { log.info("\nNo rotations found"); verbose2 && console.log("No rotations found, waiting 3 seconds..."); await new Promise((resolve) => setTimeout(resolve, 3e3)); } if (rotationsData.pending.length > 0) { log.success( "\n\u2709\uFE0F We've sent invitations to the selected engineers. The rotation will be activated once they verify their notification preferences." ); } const baseUrl = getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT }); if (rotationsData.webhookToken) { log.info( ` Use this URL to send alerts: ${baseUrl}/api/webhooks/${rotationsData.webhookToken}` ); } else { log.info(` Use this URL to send alerts: ${baseUrl}/api/webhooks/`); log.warning("No webhook token found."); } } catch (error) { console.error("Error displaying rotations:", error); process.exit(1); } process.exit(0); } async function fetchRotations(token) { try { const rotationsResponse = await fetch( `${getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT })}/api/cli/rotations`, { headers: { Authorization: `Bearer ${token}` } } ); if (!rotationsResponse.ok) { throw new Error( `Failed to fetch rotations: ${rotationsResponse.statusText}` ); } const rotationsData = await rotationsResponse.json(); return displayRotations(rotationsData); } catch (error) { log.error("Failed to fetch rotations"); if (error instanceof Error) { console.error(error.message); } else { console.error(error); } process.exit(1); } } // src/cli/git.ts import { execSync } from "child_process"; import path from "path"; import fs from "fs"; var GitError = class extends Error { constructor(message, code) { super(message); this.code = code; this.name = "GitError"; } }; var BOT_DOMAINS = [ "noreply.github.com", "users.noreply.github.com", "renovate[bot]@users.noreply.github.com", "dependabot[bot]@users.noreply.github.com", "github-actions[bot]@users.noreply.github.com", "snyk-bot@snyk.io", "security@snyk.io", "bot@renovateapp.com", "release-bot@", "builds@", "automation@", "noreply@", "actions@github.com", "dependabot-preview[bot]@users.noreply.github.com", "greenkeeper[bot]@users.noreply.github.com", "github-actions@github.com", "notifications@github.com", "semantic-release-bot@", "azure-pipelines[bot]@", "github-actions[bot]@", "dependabot[bot]@", "renovate[bot]@", "circleci@", "jenkins@", "travis@", "gitlab@", "bot@", "[bot]@" ]; var BOT_NAME_PATTERNS = [ "dependabot", "renovate", "github-actions", "snyk-bot", "semantic-release", "greenkeeper", "azure-pipelines", "circleci", "jenkins", "travis", "gitlab" ]; var BOT_PREFIXES = [ "bot-", "bot.", "ci-", "ci.", "auto-", "auto.", "build-", "build.", "test-", "automation-" ]; function isValidEmail(email) { const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(?<!\.)@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; return emailRegex.test(email) && email.includes(".") && !email.includes(".."); } function isBotEmail(email) { const lowerEmail = email.toLowerCase(); if (BOT_DOMAINS.some((domain) => lowerEmail.includes(domain))) { return true; } const username = lowerEmail.split("@")[0]; if (BOT_NAME_PATTERNS.some((pattern) => username.includes(pattern))) { return true; } if (BOT_PREFIXES.some((prefix) => username.startsWith(prefix))) { return true; } return ( // Check for UUIDs or long hexadecimal strings (common in auto-generated emails) /[0-9a-f]{8}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{12}/.test( username ) || // Check for [bot] suffix /\[bot\]$/.test(username) || // Check for exact bot username or starts with bot username === "bot" || username.startsWith("bot.") || // Check for numbered service accounts (e.g. service123456) /^(service|account|user|admin)\d{4,}$/.test(username) || // Check for automation numbers /^automation-\d+$/.test(username) ); } function parseGitEmails(gitLog, excludeDomains) { const emailMap = /* @__PURE__ */ new Map(); const lines = gitLog.split("\n"); for (const line of lines) { const bracketMatch = line.match(/<([^>]+)>/); const email = (bracketMatch ? bracketMatch[1] : line).toLowerCase().trim(); if (isValidEmail(email) && !excludeDomains.some((domain) => email.endsWith(domain))) { emailMap.set(email, { email, isBot: isBotEmail(email) }); } } return new Set(emailMap.values()); } function buildGitLogCommand(timeRange, maxCommits) { const since = /* @__PURE__ */ new Date(); since.setDate(since.getDate() - timeRange); return `git log -n ${maxCommits} --since="${since.toISOString()}" --format="%aE%n%cE"`; } function getParentDirectory(dirPath) { return path.dirname(dirPath); } function findGitRoot(startPath = process.cwd()) { let currentPath = startPath; const rootPath = path.parse(currentPath).root; while (currentPath !== rootPath) { if (fs.existsSync(path.join(currentPath, ".git"))) { return currentPath; } currentPath = getParentDirectory(currentPath); } return null; } function runGitCommand(command, gitRoot) { try { return execSync(command, { cwd: gitRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large repos }); } catch (error) { if (error instanceof Error) { throw new GitError( `Git command failed: ${error.message}`, "COMMAND_FAILED" ); } throw error; } } async function getConfigEmail(gitRoot) { try { const email = runGitCommand("git config user.email", gitRoot).trim(); return isValidEmail(email) ? email : null; } catch { return null; } } async function getGitEmails(options = {}) { const { timeRange = 365, maxCommits = 1e3, excludeDomains = ["noreply.github.com"], includeConfigEmail = true } = options; const gitRoot = findGitRoot(); if (!gitRoot) { throw new GitError( "Not a git repository (or any of the parent directories)", "NO_GIT_REPO" ); } try { const gitLog = runGitCommand( buildGitLogCommand(timeRange, maxCommits), gitRoot ); const emailSet = parseGitEmails(gitLog, excludeDomains); if (emailSet.size === 0 && includeConfigEmail) { const configEmail = await getConfigEmail(gitRoot); if (configEmail) { emailSet.add({ email: configEmail, isBot: isBotEmail(configEmail) }); } } if (emailSet.size === 0) { throw new GitError( "No valid email addresses found in git history", "NO_EMAILS_FOUND" ); } return Array.from(emailSet); } catch (error) { if (error instanceof GitError) { throw error; } throw new GitError( `Failed to get git emails: ${error instanceof Error ? error.message : "Unknown error"}`, "UNKNOWN_ERROR" ); } } // src/cli/checkout.ts import open from "open"; async function waitForSubscription(token) { let attempts = 0; const maxAttempts = 3 * 60; while (attempts < maxAttempts) { verbose2 && console.log("Waiting for subscription...", attempts); const response = await fetch( `${getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT })}/api/cli/subscription`, { headers: { Authorization: `Bearer ${token}` } } ); if (!response.ok) { verbose2 && console.log( `Subscription check failed: ${response.status} ${response.statusText}` ); await new Promise((resolve) => setTimeout(resolve, 1e3)); attempts++; continue; } const data = await response.json(); if (data.hasActiveSubscription) { return true; } await new Promise((resolve) => setTimeout(resolve, 1e3)); attempts++; } return false; } async function ensureHasSubscription(email, token) { const response = await fetch( `${getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT })}/api/cli/subscription`, { headers: { Authorization: `Bearer ${token}` } } ); if (!response.ok) { verbose2 && console.log( `Subscription check failed: ${response.status} ${response.statusText}` ); log.warning( "Could not verify subscription status. Proceeding with checkout." ); } else { const data = await response.json(); if (data.ok && data.hasActiveSubscription) { verbose2 && console.log("Active subscription found"); return true; } else { verbose2 && console.log("No active subscription found"); } } log.title("\n\u{1F4B3} Payment Required"); log.info("\nStarting Stripe checkout..."); try { const checkoutResponse = await fetch( `${getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT })}/api/stripe/session`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ email }) } ); if (!checkoutResponse.ok) { console.log( "baseUrl", getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT }) ); console.log(await checkoutResponse.text()); throw new Error("Failed to create checkout session"); } const { url } = await checkoutResponse.json(); if (!url) { throw new Error("No checkout URL received"); } log.info("\nOpening checkout page..."); log.info(` Checkout page URL ${url}`); await open(url); verbose2 && log.info("\nWaiting for subscription completion..."); const success = await waitForSubscription(token); if (!success) { log.error("\nSubscription timeout. Please try again."); return false; } log.success("\n\u2728 Subscription activated!"); return true; } catch (error) { if (error instanceof Error) { log.error(` Checkout failed: ${error.message}`); } else { log.error("\nCheckout failed: Unknown error"); } return false; } } // src/cli/bin.ts import ora from "ora"; async function setup() { const { token, userId, email } = await ensureLoggedIn(); if (!userId || !token) { log.error("Not logged in"); process.exit(1); } const subscriptionResponse = await fetch( `${getBaseUrl({ prodUrl: !process.env.BEEPS_DEVELOPMENT })}/api/cli/subscription`, { headers: { Authorization: `Bearer ${token}` } } ); if (!subscriptionResponse.ok) { log.error("Failed to check subscription status"); process.exit(1); } const subscriptionData = await subscriptionResponse.json(); if (!subscriptionData.ok || !subscriptionData.hasActiveSubscription) { const success = await ensureHasSubscription(email || "", token); if (!success) { log.error("Could not set up subscription"); process.exit(1); } } const command = process.argv[2]; if (command === "alerts") { return fetchAlerts(token); } const hasConfigFile = await hasRotationsConfigFile(); if (hasConfigFile) { const config = readConfigurationFile(); if (!config) { log.error("Failed to read beeps.json"); process.exit(1); } await pushConfiguration(token, userId, config); log.success("\n beeps.json pushed"); return fetchRotations(token); } log.title("\n\u{1F465} Setting Up Rotation"); log.info("\nFetching engineers from git history..."); const gitEmails = await getGitEmails(); const emails = gitEmails.filter(({ isBot }) => !isBot).map(({ email: email2 }) => email2); const { select: selectEngineers } = await import("inquirer-select-pro"); const selectedMembers = await selectEngineers({ message: "Select engineers for the rotation:", options: emails.map((email2) => ({ value: email2, name: email2 })), multiple: true, clearInputWhenSelected: true, canToggleAll: true, defaultValue: emails }); if (!selectedMembers.length) { log.error("You must select at least one engineer for the rotation"); process.exit(1); } await writeDefaultRotationsConfigFile({ emails: selectedMembers }); try { const config = { rotations: [ { name: "default", members: selectedMembers, startDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0], frequency: "daily" } ] }; await pushConfiguration(token, userId, config); } catch (error) { log.error("Failed to create rotations"); process.exit(1); } const spinner = ora("Processing configuration...").start(); await new Promise((resolve) => setTimeout(resolve, 3e3)); spinner.stop(); return fetchRotations(token); } setup().catch((error) => { console.error("Error during setup:", error); process.exit(1); }); //# sourceMappingURL=bin.js.map