ramadan-cli
Advanced tools
| #!/usr/bin/env node | ||
| // src/recommendations.ts | ||
| var countryMethodMap = { | ||
| "United States": 2, | ||
| USA: 2, | ||
| US: 2, | ||
| Canada: 2, | ||
| Mexico: 2, | ||
| Pakistan: 1, | ||
| Bangladesh: 1, | ||
| India: 1, | ||
| Afghanistan: 1, | ||
| "United Kingdom": 3, | ||
| UK: 3, | ||
| Germany: 3, | ||
| Netherlands: 3, | ||
| Belgium: 3, | ||
| Sweden: 3, | ||
| Norway: 3, | ||
| Denmark: 3, | ||
| Finland: 3, | ||
| Austria: 3, | ||
| Switzerland: 3, | ||
| Poland: 3, | ||
| Italy: 3, | ||
| Spain: 3, | ||
| Greece: 3, | ||
| Japan: 3, | ||
| China: 3, | ||
| "South Korea": 3, | ||
| Australia: 3, | ||
| "New Zealand": 3, | ||
| "South Africa": 3, | ||
| "Saudi Arabia": 4, | ||
| Yemen: 4, | ||
| Oman: 4, | ||
| Bahrain: 4, | ||
| Egypt: 5, | ||
| Syria: 5, | ||
| Lebanon: 5, | ||
| Palestine: 5, | ||
| Jordan: 5, | ||
| Iraq: 5, | ||
| Libya: 5, | ||
| Sudan: 5, | ||
| Iran: 7, | ||
| Kuwait: 9, | ||
| Qatar: 10, | ||
| Singapore: 11, | ||
| France: 12, | ||
| Turkey: 13, | ||
| T\u00FCrkiye: 13, | ||
| Russia: 14, | ||
| "United Arab Emirates": 16, | ||
| UAE: 16, | ||
| Malaysia: 17, | ||
| Brunei: 17, | ||
| Tunisia: 18, | ||
| Algeria: 19, | ||
| Indonesia: 20, | ||
| Morocco: 21, | ||
| Portugal: 22 | ||
| }; | ||
| var hanafiCountries = /* @__PURE__ */ new Set([ | ||
| "Pakistan", | ||
| "Bangladesh", | ||
| "India", | ||
| "Afghanistan", | ||
| "Turkey", | ||
| "T\xFCrkiye", | ||
| "Iraq", | ||
| "Syria", | ||
| "Jordan", | ||
| "Palestine", | ||
| "Kazakhstan", | ||
| "Uzbekistan", | ||
| "Tajikistan", | ||
| "Turkmenistan", | ||
| "Kyrgyzstan" | ||
| ]); | ||
| var getRecommendedMethod = (country) => { | ||
| const direct = countryMethodMap[country]; | ||
| if (direct !== void 0) { | ||
| return direct; | ||
| } | ||
| const lowerCountry = country.toLowerCase(); | ||
| for (const [key, value] of Object.entries(countryMethodMap)) { | ||
| if (key.toLowerCase() === lowerCountry) { | ||
| return value; | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
| var getRecommendedSchool = (country) => { | ||
| if (hanafiCountries.has(country)) { | ||
| return 1; | ||
| } | ||
| const lowerCountry = country.toLowerCase(); | ||
| for (const listedCountry of hanafiCountries) { | ||
| if (listedCountry.toLowerCase() === lowerCountry) { | ||
| return 1; | ||
| } | ||
| } | ||
| return 0; | ||
| }; | ||
| // src/ramadan-config.ts | ||
| import Conf from "conf"; | ||
| import { z } from "zod"; | ||
| var SharedConfigSchema = z.object({ | ||
| latitude: z.number().optional(), | ||
| longitude: z.number().optional(), | ||
| city: z.string().optional(), | ||
| country: z.string().optional(), | ||
| method: z.number().optional(), | ||
| school: z.number().optional(), | ||
| timezone: z.string().optional(), | ||
| firstRozaDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), | ||
| format24h: z.boolean().optional() | ||
| }); | ||
| var IsoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/); | ||
| var DEFAULT_METHOD = 2; | ||
| var DEFAULT_SCHOOL = 0; | ||
| var getConfigCwd = () => { | ||
| const configuredPath = process.env.RAMADAN_CLI_CONFIG_DIR; | ||
| if (configuredPath) { | ||
| return configuredPath; | ||
| } | ||
| const isTestRuntime = process.env.VITEST === "true" || process.env.NODE_ENV === "test"; | ||
| if (isTestRuntime) { | ||
| return "/tmp"; | ||
| } | ||
| return void 0; | ||
| }; | ||
| var shouldApplyRecommendedMethod = (currentMethod, recommendedMethod) => currentMethod === DEFAULT_METHOD || currentMethod === recommendedMethod; | ||
| var shouldApplyRecommendedSchool = (currentSchool, recommendedSchool) => currentSchool === DEFAULT_SCHOOL || currentSchool === recommendedSchool; | ||
| var configCwd = getConfigCwd(); | ||
| var sharedConfig = new Conf({ | ||
| projectName: "ramadan-cli", | ||
| ...configCwd ? { cwd: configCwd } : {}, | ||
| defaults: { | ||
| method: DEFAULT_METHOD, | ||
| school: DEFAULT_SCHOOL, | ||
| format24h: false | ||
| } | ||
| }); | ||
| var legacyAzaanConfig = new Conf({ | ||
| projectName: "azaan", | ||
| ...configCwd ? { cwd: configCwd } : {} | ||
| }); | ||
| var getValidatedStore = () => { | ||
| const parsed = SharedConfigSchema.safeParse(sharedConfig.store); | ||
| if (!parsed.success) { | ||
| return { | ||
| method: DEFAULT_METHOD, | ||
| school: DEFAULT_SCHOOL, | ||
| format24h: false | ||
| }; | ||
| } | ||
| return parsed.data; | ||
| }; | ||
| var getStoredLocation = () => { | ||
| const store = getValidatedStore(); | ||
| return { | ||
| city: store.city, | ||
| country: store.country, | ||
| latitude: store.latitude, | ||
| longitude: store.longitude | ||
| }; | ||
| }; | ||
| var hasStoredLocation = () => { | ||
| const location = getStoredLocation(); | ||
| const hasCityCountry = Boolean(location.city && location.country); | ||
| const hasCoords = Boolean( | ||
| location.latitude !== void 0 && location.longitude !== void 0 | ||
| ); | ||
| return hasCityCountry || hasCoords; | ||
| }; | ||
| var getStoredPrayerSettings = () => { | ||
| const store = getValidatedStore(); | ||
| return { | ||
| method: store.method ?? DEFAULT_METHOD, | ||
| school: store.school ?? DEFAULT_SCHOOL, | ||
| timezone: store.timezone | ||
| }; | ||
| }; | ||
| var setStoredLocation = (location) => { | ||
| if (location.city) { | ||
| sharedConfig.set("city", location.city); | ||
| } | ||
| if (location.country) { | ||
| sharedConfig.set("country", location.country); | ||
| } | ||
| if (location.latitude !== void 0) { | ||
| sharedConfig.set("latitude", location.latitude); | ||
| } | ||
| if (location.longitude !== void 0) { | ||
| sharedConfig.set("longitude", location.longitude); | ||
| } | ||
| }; | ||
| var setStoredTimezone = (timezone) => { | ||
| if (!timezone) { | ||
| return; | ||
| } | ||
| sharedConfig.set("timezone", timezone); | ||
| }; | ||
| var setStoredMethod = (method) => { | ||
| sharedConfig.set("method", method); | ||
| }; | ||
| var setStoredSchool = (school) => { | ||
| sharedConfig.set("school", school); | ||
| }; | ||
| var getStoredFirstRozaDate = () => { | ||
| const store = getValidatedStore(); | ||
| return store.firstRozaDate; | ||
| }; | ||
| var setStoredFirstRozaDate = (firstRozaDate) => { | ||
| const parsed = IsoDateSchema.safeParse(firstRozaDate); | ||
| if (!parsed.success) { | ||
| throw new Error("Invalid first roza date. Use YYYY-MM-DD."); | ||
| } | ||
| sharedConfig.set("firstRozaDate", parsed.data); | ||
| }; | ||
| var clearStoredFirstRozaDate = () => { | ||
| sharedConfig.delete("firstRozaDate"); | ||
| }; | ||
| var clearRamadanConfig = () => { | ||
| sharedConfig.clear(); | ||
| legacyAzaanConfig.clear(); | ||
| }; | ||
| var maybeSetRecommendedMethod = (country) => { | ||
| const recommendedMethod = getRecommendedMethod(country); | ||
| if (recommendedMethod === null) { | ||
| return; | ||
| } | ||
| const currentMethod = sharedConfig.get("method") ?? DEFAULT_METHOD; | ||
| if (!shouldApplyRecommendedMethod(currentMethod, recommendedMethod)) { | ||
| return; | ||
| } | ||
| sharedConfig.set("method", recommendedMethod); | ||
| }; | ||
| var maybeSetRecommendedSchool = (country) => { | ||
| const currentSchool = sharedConfig.get("school") ?? DEFAULT_SCHOOL; | ||
| const recommendedSchool = getRecommendedSchool(country); | ||
| if (!shouldApplyRecommendedSchool(currentSchool, recommendedSchool)) { | ||
| return; | ||
| } | ||
| sharedConfig.set("school", recommendedSchool); | ||
| }; | ||
| var saveAutoDetectedSetup = (location) => { | ||
| setStoredLocation({ | ||
| city: location.city, | ||
| country: location.country, | ||
| latitude: location.latitude, | ||
| longitude: location.longitude | ||
| }); | ||
| setStoredTimezone(location.timezone); | ||
| maybeSetRecommendedMethod(location.country); | ||
| maybeSetRecommendedSchool(location.country); | ||
| }; | ||
| var applyRecommendedSettingsIfUnset = (country) => { | ||
| maybeSetRecommendedMethod(country); | ||
| maybeSetRecommendedSchool(country); | ||
| }; | ||
| // src/commands/config.ts | ||
| import pc from "picocolors"; | ||
| import { z as z2 } from "zod"; | ||
| var MethodSchema = z2.coerce.number().int().min(0).max(23); | ||
| var SchoolSchema = z2.coerce.number().int().min(0).max(1); | ||
| var LatitudeSchema = z2.coerce.number().min(-90).max(90); | ||
| var LongitudeSchema = z2.coerce.number().min(-180).max(180); | ||
| var parseOptionalWithSchema = (value, schema, label) => { | ||
| if (value === void 0) { | ||
| return void 0; | ||
| } | ||
| const parsed = schema.safeParse(value); | ||
| if (!parsed.success) { | ||
| throw new Error(`Invalid ${label}.`); | ||
| } | ||
| return parsed.data; | ||
| }; | ||
| var parseConfigUpdates = (options) => ({ | ||
| ...options.city ? { city: options.city.trim() } : {}, | ||
| ...options.country ? { country: options.country.trim() } : {}, | ||
| ...options.latitude !== void 0 ? { | ||
| latitude: parseOptionalWithSchema( | ||
| options.latitude, | ||
| LatitudeSchema, | ||
| "latitude" | ||
| ) | ||
| } : {}, | ||
| ...options.longitude !== void 0 ? { | ||
| longitude: parseOptionalWithSchema( | ||
| options.longitude, | ||
| LongitudeSchema, | ||
| "longitude" | ||
| ) | ||
| } : {}, | ||
| ...options.method !== void 0 ? { | ||
| method: parseOptionalWithSchema(options.method, MethodSchema, "method") | ||
| } : {}, | ||
| ...options.school !== void 0 ? { | ||
| school: parseOptionalWithSchema(options.school, SchoolSchema, "school") | ||
| } : {}, | ||
| ...options.timezone ? { timezone: options.timezone.trim() } : {} | ||
| }); | ||
| var mergeLocationUpdates = (current, updates) => ({ | ||
| ...updates.city !== void 0 ? { city: updates.city } : current.city ? { city: current.city } : {}, | ||
| ...updates.country !== void 0 ? { country: updates.country } : current.country ? { country: current.country } : {}, | ||
| ...updates.latitude !== void 0 ? { latitude: updates.latitude } : current.latitude !== void 0 ? { latitude: current.latitude } : {}, | ||
| ...updates.longitude !== void 0 ? { longitude: updates.longitude } : current.longitude !== void 0 ? { longitude: current.longitude } : {} | ||
| }); | ||
| var printCurrentConfig = () => { | ||
| const location = getStoredLocation(); | ||
| const settings = getStoredPrayerSettings(); | ||
| const firstRozaDate = getStoredFirstRozaDate(); | ||
| console.log(pc.dim("Current configuration:")); | ||
| if (location.city) { | ||
| console.log(` City: ${location.city}`); | ||
| } | ||
| if (location.country) { | ||
| console.log(` Country: ${location.country}`); | ||
| } | ||
| if (location.latitude !== void 0) { | ||
| console.log(` Latitude: ${location.latitude}`); | ||
| } | ||
| if (location.longitude !== void 0) { | ||
| console.log(` Longitude: ${location.longitude}`); | ||
| } | ||
| console.log(` Method: ${settings.method}`); | ||
| console.log(` School: ${settings.school}`); | ||
| if (settings.timezone) { | ||
| console.log(` Timezone: ${settings.timezone}`); | ||
| } | ||
| if (firstRozaDate) { | ||
| console.log(` First Roza Date: ${firstRozaDate}`); | ||
| } | ||
| }; | ||
| var hasConfigUpdateFlags = (options) => Boolean( | ||
| options.city || options.country || options.latitude !== void 0 || options.longitude !== void 0 || options.method !== void 0 || options.school !== void 0 || options.timezone | ||
| ); | ||
| var configCommand = async (options) => { | ||
| if (options.clear) { | ||
| clearRamadanConfig(); | ||
| console.log(pc.green("Configuration cleared.")); | ||
| return; | ||
| } | ||
| if (options.show) { | ||
| printCurrentConfig(); | ||
| return; | ||
| } | ||
| if (!hasConfigUpdateFlags(options)) { | ||
| console.log( | ||
| pc.dim( | ||
| "No config updates provided. Use `ramadan-cli config --show` to inspect." | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const updates = parseConfigUpdates(options); | ||
| const currentLocation = getStoredLocation(); | ||
| const nextLocation = mergeLocationUpdates(currentLocation, updates); | ||
| setStoredLocation(nextLocation); | ||
| if (updates.method !== void 0) { | ||
| setStoredMethod(updates.method); | ||
| } | ||
| if (updates.school !== void 0) { | ||
| setStoredSchool(updates.school); | ||
| } | ||
| if (updates.timezone) { | ||
| setStoredTimezone(updates.timezone); | ||
| } | ||
| console.log(pc.green("Configuration updated.")); | ||
| }; | ||
| // src/api.ts | ||
| import { z as z3 } from "zod"; | ||
| var API_BASE = "https://api.aladhan.com/v1"; | ||
| var PrayerTimingsSchema = z3.object({ | ||
| Fajr: z3.string(), | ||
| Sunrise: z3.string(), | ||
| Dhuhr: z3.string(), | ||
| Asr: z3.string(), | ||
| Sunset: z3.string(), | ||
| Maghrib: z3.string(), | ||
| Isha: z3.string(), | ||
| Imsak: z3.string(), | ||
| Midnight: z3.string(), | ||
| Firstthird: z3.string(), | ||
| Lastthird: z3.string() | ||
| }); | ||
| var HijriDateSchema = z3.object({ | ||
| date: z3.string(), | ||
| day: z3.string(), | ||
| month: z3.object({ | ||
| number: z3.number(), | ||
| en: z3.string(), | ||
| ar: z3.string() | ||
| }), | ||
| year: z3.string(), | ||
| weekday: z3.object({ | ||
| en: z3.string(), | ||
| ar: z3.string() | ||
| }) | ||
| }); | ||
| var GregorianDateSchema = z3.object({ | ||
| date: z3.string(), | ||
| day: z3.string(), | ||
| month: z3.object({ | ||
| number: z3.number(), | ||
| en: z3.string() | ||
| }), | ||
| year: z3.string(), | ||
| weekday: z3.object({ | ||
| en: z3.string() | ||
| }) | ||
| }); | ||
| var PrayerMetaSchema = z3.object({ | ||
| latitude: z3.number(), | ||
| longitude: z3.number(), | ||
| timezone: z3.string(), | ||
| method: z3.object({ | ||
| id: z3.number(), | ||
| name: z3.string() | ||
| }), | ||
| school: z3.union([ | ||
| z3.object({ | ||
| id: z3.number(), | ||
| name: z3.string() | ||
| }), | ||
| z3.string() | ||
| ]) | ||
| }); | ||
| var PrayerDataSchema = z3.object({ | ||
| timings: PrayerTimingsSchema, | ||
| date: z3.object({ | ||
| readable: z3.string(), | ||
| timestamp: z3.string(), | ||
| hijri: HijriDateSchema, | ||
| gregorian: GregorianDateSchema | ||
| }), | ||
| meta: PrayerMetaSchema | ||
| }); | ||
| var NextPrayerDataSchema = z3.object({ | ||
| timings: PrayerTimingsSchema, | ||
| date: z3.object({ | ||
| readable: z3.string(), | ||
| timestamp: z3.string(), | ||
| hijri: HijriDateSchema, | ||
| gregorian: GregorianDateSchema | ||
| }), | ||
| meta: PrayerMetaSchema, | ||
| nextPrayer: z3.string(), | ||
| nextPrayerTime: z3.string() | ||
| }); | ||
| var CalculationMethodSchema = z3.object({ | ||
| id: z3.number(), | ||
| name: z3.string(), | ||
| params: z3.object({ | ||
| Fajr: z3.number(), | ||
| Isha: z3.union([z3.number(), z3.string()]) | ||
| }) | ||
| }); | ||
| var QiblaDataSchema = z3.object({ | ||
| latitude: z3.number(), | ||
| longitude: z3.number(), | ||
| direction: z3.number() | ||
| }); | ||
| var ApiEnvelopeSchema = z3.object({ | ||
| code: z3.number(), | ||
| status: z3.string(), | ||
| data: z3.unknown() | ||
| }); | ||
| var formatDate = (date) => { | ||
| const day = String(date.getDate()).padStart(2, "0"); | ||
| const month = String(date.getMonth() + 1).padStart(2, "0"); | ||
| const year = date.getFullYear(); | ||
| return `${day}-${month}-${year}`; | ||
| }; | ||
| var parseApiResponse = (payload, dataSchema) => { | ||
| const parsedEnvelope = ApiEnvelopeSchema.safeParse(payload); | ||
| if (!parsedEnvelope.success) { | ||
| throw new Error( | ||
| `Invalid API response: ${parsedEnvelope.error.issues[0]?.message ?? "Unknown schema mismatch"}` | ||
| ); | ||
| } | ||
| if (parsedEnvelope.data.code !== 200) { | ||
| throw new Error( | ||
| `API ${parsedEnvelope.data.code}: ${parsedEnvelope.data.status}` | ||
| ); | ||
| } | ||
| if (typeof parsedEnvelope.data.data === "string") { | ||
| throw new Error(`API returned message: ${parsedEnvelope.data.data}`); | ||
| } | ||
| const parsedData = dataSchema.safeParse(parsedEnvelope.data.data); | ||
| if (!parsedData.success) { | ||
| throw new Error( | ||
| `Invalid API response: ${parsedData.error.issues[0]?.message ?? "Unknown schema mismatch"}` | ||
| ); | ||
| } | ||
| return parsedData.data; | ||
| }; | ||
| var fetchAndParse = async (url, dataSchema) => { | ||
| const response = await fetch(url); | ||
| const json = await response.json(); | ||
| return parseApiResponse(json, dataSchema); | ||
| }; | ||
| var fetchTimingsByCity = async (opts) => { | ||
| const date = formatDate(opts.date ?? /* @__PURE__ */ new Date()); | ||
| const params = new URLSearchParams({ | ||
| city: opts.city, | ||
| country: opts.country | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/timingsByCity/${date}?${params}`, | ||
| PrayerDataSchema | ||
| ); | ||
| }; | ||
| var fetchTimingsByAddress = async (opts) => { | ||
| const date = formatDate(opts.date ?? /* @__PURE__ */ new Date()); | ||
| const params = new URLSearchParams({ | ||
| address: opts.address | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/timingsByAddress/${date}?${params}`, | ||
| PrayerDataSchema | ||
| ); | ||
| }; | ||
| var fetchTimingsByCoords = async (opts) => { | ||
| const date = formatDate(opts.date ?? /* @__PURE__ */ new Date()); | ||
| const params = new URLSearchParams({ | ||
| latitude: String(opts.latitude), | ||
| longitude: String(opts.longitude) | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| if (opts.timezone) { | ||
| params.set("timezonestring", opts.timezone); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/timings/${date}?${params}`, | ||
| PrayerDataSchema | ||
| ); | ||
| }; | ||
| var fetchNextPrayer = async (opts) => { | ||
| const date = formatDate(/* @__PURE__ */ new Date()); | ||
| const params = new URLSearchParams({ | ||
| latitude: String(opts.latitude), | ||
| longitude: String(opts.longitude) | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| if (opts.timezone) { | ||
| params.set("timezonestring", opts.timezone); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/nextPrayer/${date}?${params}`, | ||
| NextPrayerDataSchema | ||
| ); | ||
| }; | ||
| var fetchCalendarByCity = async (opts) => { | ||
| const params = new URLSearchParams({ | ||
| city: opts.city, | ||
| country: opts.country | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| const path = opts.month ? `${opts.year}/${opts.month}` : String(opts.year); | ||
| return fetchAndParse( | ||
| `${API_BASE}/calendarByCity/${path}?${params}`, | ||
| z3.array(PrayerDataSchema) | ||
| ); | ||
| }; | ||
| var fetchCalendarByAddress = async (opts) => { | ||
| const params = new URLSearchParams({ | ||
| address: opts.address | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| const path = opts.month ? `${opts.year}/${opts.month}` : String(opts.year); | ||
| return fetchAndParse( | ||
| `${API_BASE}/calendarByAddress/${path}?${params}`, | ||
| z3.array(PrayerDataSchema) | ||
| ); | ||
| }; | ||
| var fetchHijriCalendarByAddress = async (opts) => { | ||
| const params = new URLSearchParams({ | ||
| address: opts.address | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/hijriCalendarByAddress/${opts.year}/${opts.month}?${params}`, | ||
| z3.array(PrayerDataSchema) | ||
| ); | ||
| }; | ||
| var fetchHijriCalendarByCity = async (opts) => { | ||
| const params = new URLSearchParams({ | ||
| city: opts.city, | ||
| country: opts.country | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/hijriCalendarByCity/${opts.year}/${opts.month}?${params}`, | ||
| z3.array(PrayerDataSchema) | ||
| ); | ||
| }; | ||
| var fetchMethods = async () => fetchAndParse( | ||
| `${API_BASE}/methods`, | ||
| z3.record(z3.string(), CalculationMethodSchema) | ||
| ); | ||
| var fetchQibla = async (latitude, longitude) => fetchAndParse(`${API_BASE}/qibla/${latitude}/${longitude}`, QiblaDataSchema); | ||
| // src/geo.ts | ||
| import { z as z4 } from "zod"; | ||
| var IpApiSchema = z4.object({ | ||
| city: z4.string(), | ||
| country: z4.string(), | ||
| lat: z4.number(), | ||
| lon: z4.number(), | ||
| timezone: z4.string().optional() | ||
| }); | ||
| var IpapiCoSchema = z4.object({ | ||
| city: z4.string(), | ||
| country_name: z4.string(), | ||
| latitude: z4.number(), | ||
| longitude: z4.number(), | ||
| timezone: z4.string().optional() | ||
| }); | ||
| var IpWhoisSchema = z4.object({ | ||
| success: z4.boolean(), | ||
| city: z4.string(), | ||
| country: z4.string(), | ||
| latitude: z4.number(), | ||
| longitude: z4.number(), | ||
| timezone: z4.object({ | ||
| id: z4.string().optional() | ||
| }).optional() | ||
| }); | ||
| var OpenMeteoSearchSchema = z4.object({ | ||
| results: z4.array( | ||
| z4.object({ | ||
| name: z4.string(), | ||
| country: z4.string(), | ||
| latitude: z4.number(), | ||
| longitude: z4.number(), | ||
| timezone: z4.string().optional() | ||
| }) | ||
| ).optional() | ||
| }); | ||
| var fetchJson = async (url) => { | ||
| const response = await fetch(url); | ||
| return await response.json(); | ||
| }; | ||
| var tryIpApi = async () => { | ||
| try { | ||
| const json = await fetchJson( | ||
| "http://ip-api.com/json/?fields=city,country,lat,lon,timezone" | ||
| ); | ||
| const parsed = IpApiSchema.safeParse(json); | ||
| if (!parsed.success) { | ||
| return null; | ||
| } | ||
| return { | ||
| city: parsed.data.city, | ||
| country: parsed.data.country, | ||
| latitude: parsed.data.lat, | ||
| longitude: parsed.data.lon, | ||
| timezone: parsed.data.timezone ?? "" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| var tryIpapiCo = async () => { | ||
| try { | ||
| const json = await fetchJson("https://ipapi.co/json/"); | ||
| const parsed = IpapiCoSchema.safeParse(json); | ||
| if (!parsed.success) { | ||
| return null; | ||
| } | ||
| return { | ||
| city: parsed.data.city, | ||
| country: parsed.data.country_name, | ||
| latitude: parsed.data.latitude, | ||
| longitude: parsed.data.longitude, | ||
| timezone: parsed.data.timezone ?? "" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| var tryIpWhois = async () => { | ||
| try { | ||
| const json = await fetchJson("https://ipwho.is/"); | ||
| const parsed = IpWhoisSchema.safeParse(json); | ||
| if (!parsed.success) { | ||
| return null; | ||
| } | ||
| if (!parsed.data.success) { | ||
| return null; | ||
| } | ||
| return { | ||
| city: parsed.data.city, | ||
| country: parsed.data.country, | ||
| latitude: parsed.data.latitude, | ||
| longitude: parsed.data.longitude, | ||
| timezone: parsed.data.timezone?.id ?? "" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| var guessLocation = async () => { | ||
| const fromIpApi = await tryIpApi(); | ||
| if (fromIpApi) { | ||
| return fromIpApi; | ||
| } | ||
| const fromIpapi = await tryIpapiCo(); | ||
| if (fromIpapi) { | ||
| return fromIpapi; | ||
| } | ||
| return tryIpWhois(); | ||
| }; | ||
| var guessCityCountry = async (query) => { | ||
| const trimmedQuery = query.trim(); | ||
| if (!trimmedQuery) { | ||
| return null; | ||
| } | ||
| try { | ||
| const url = new URL("https://geocoding-api.open-meteo.com/v1/search"); | ||
| url.searchParams.set("name", trimmedQuery); | ||
| url.searchParams.set("count", "1"); | ||
| url.searchParams.set("language", "en"); | ||
| url.searchParams.set("format", "json"); | ||
| const json = await fetchJson(url.toString()); | ||
| const parsed = OpenMeteoSearchSchema.safeParse(json); | ||
| if (!parsed.success) { | ||
| return null; | ||
| } | ||
| const result = parsed.data.results?.[0]; | ||
| if (!result) { | ||
| return null; | ||
| } | ||
| return { | ||
| city: result.name, | ||
| country: result.country, | ||
| latitude: result.latitude, | ||
| longitude: result.longitude, | ||
| ...result.timezone ? { timezone: result.timezone } : {} | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| // src/commands/ramadan.ts | ||
| import ora from "ora"; | ||
| import pc4 from "picocolors"; | ||
| // src/setup.ts | ||
| import * as p from "@clack/prompts"; | ||
| // src/ui/theme.ts | ||
| import pc2 from "picocolors"; | ||
| var MOON_EMOJI = "\u{1F319}"; | ||
| var RAMADAN_GREEN_RGB = "38;2;128;240;151"; | ||
| var ANSI_RESET = "\x1B[0m"; | ||
| var supportsTrueColor = () => { | ||
| const colorTerm = process.env.COLORTERM?.toLowerCase() ?? ""; | ||
| return colorTerm.includes("truecolor") || colorTerm.includes("24bit"); | ||
| }; | ||
| var ramadanGreen = (value) => { | ||
| if (!pc2.isColorSupported) { | ||
| return value; | ||
| } | ||
| if (!supportsTrueColor()) { | ||
| return pc2.green(value); | ||
| } | ||
| return `\x1B[${RAMADAN_GREEN_RGB}m${value}${ANSI_RESET}`; | ||
| }; | ||
| // src/setup.ts | ||
| var METHOD_OPTIONS = [ | ||
| { value: 0, label: "Jafari (Shia Ithna-Ashari)" }, | ||
| { value: 1, label: "Karachi (Pakistan)" }, | ||
| { value: 2, label: "ISNA (North America)" }, | ||
| { value: 3, label: "MWL (Muslim World League)" }, | ||
| { value: 4, label: "Makkah (Umm al-Qura)" }, | ||
| { value: 5, label: "Egypt" }, | ||
| { value: 7, label: "Tehran (Shia)" }, | ||
| { value: 8, label: "Gulf Region" }, | ||
| { value: 9, label: "Kuwait" }, | ||
| { value: 10, label: "Qatar" }, | ||
| { value: 11, label: "Singapore" }, | ||
| { value: 12, label: "France" }, | ||
| { value: 13, label: "Turkey" }, | ||
| { value: 14, label: "Russia" }, | ||
| { value: 15, label: "Moonsighting Committee" }, | ||
| { value: 16, label: "Dubai" }, | ||
| { value: 17, label: "Malaysia (JAKIM)" }, | ||
| { value: 18, label: "Tunisia" }, | ||
| { value: 19, label: "Algeria" }, | ||
| { value: 20, label: "Indonesia" }, | ||
| { value: 21, label: "Morocco" }, | ||
| { value: 22, label: "Portugal" }, | ||
| { value: 23, label: "Jordan" } | ||
| ]; | ||
| var SCHOOL_SHAFI = 0; | ||
| var SCHOOL_HANAFI = 1; | ||
| var DEFAULT_METHOD2 = 2; | ||
| var normalize = (value) => value.trim().toLowerCase(); | ||
| var toNonEmptyString = (value) => { | ||
| if (typeof value !== "string") { | ||
| return null; | ||
| } | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| return null; | ||
| } | ||
| return trimmed; | ||
| }; | ||
| var toNumberSelection = (value) => { | ||
| if (typeof value !== "number") { | ||
| return null; | ||
| } | ||
| return value; | ||
| }; | ||
| var toTimezoneChoice = (value, hasDetectedOption) => { | ||
| if (value === "custom") { | ||
| return "custom"; | ||
| } | ||
| if (value === "skip") { | ||
| return "skip"; | ||
| } | ||
| if (hasDetectedOption && value === "detected") { | ||
| return "detected"; | ||
| } | ||
| return null; | ||
| }; | ||
| var findMethodLabel = (method) => { | ||
| const option = METHOD_OPTIONS.find((entry) => entry.value === method); | ||
| if (option) { | ||
| return option.label; | ||
| } | ||
| return `Method ${method}`; | ||
| }; | ||
| var getMethodOptions = (recommendedMethod) => { | ||
| if (recommendedMethod === null) { | ||
| return METHOD_OPTIONS; | ||
| } | ||
| const recommendedOption = { | ||
| value: recommendedMethod, | ||
| label: `${findMethodLabel(recommendedMethod)} (Recommended)`, | ||
| hint: "Based on your country" | ||
| }; | ||
| const remaining = METHOD_OPTIONS.filter( | ||
| (option) => option.value !== recommendedMethod | ||
| ); | ||
| return [recommendedOption, ...remaining]; | ||
| }; | ||
| var getSchoolOptions = (recommendedSchool) => { | ||
| if (recommendedSchool === SCHOOL_HANAFI) { | ||
| return [ | ||
| { | ||
| value: SCHOOL_HANAFI, | ||
| label: "Hanafi (Recommended)", | ||
| hint: "Later Asr timing" | ||
| }, | ||
| { | ||
| value: SCHOOL_SHAFI, | ||
| label: "Shafi", | ||
| hint: "Standard Asr timing" | ||
| } | ||
| ]; | ||
| } | ||
| return [ | ||
| { | ||
| value: SCHOOL_SHAFI, | ||
| label: "Shafi (Recommended)", | ||
| hint: "Standard Asr timing" | ||
| }, | ||
| { | ||
| value: SCHOOL_HANAFI, | ||
| label: "Hanafi", | ||
| hint: "Later Asr timing" | ||
| } | ||
| ]; | ||
| }; | ||
| var cityCountryMatchesGuess = (city, country, guess) => normalize(city) === normalize(guess.city) && normalize(country) === normalize(guess.country); | ||
| var resolveDetectedDetails = async (city, country, ipGuess) => { | ||
| const geocoded = await guessCityCountry(`${city}, ${country}`); | ||
| if (geocoded) { | ||
| return { | ||
| latitude: geocoded.latitude, | ||
| longitude: geocoded.longitude, | ||
| timezone: geocoded.timezone | ||
| }; | ||
| } | ||
| if (!ipGuess) { | ||
| return {}; | ||
| } | ||
| if (!cityCountryMatchesGuess(city, country, ipGuess)) { | ||
| return {}; | ||
| } | ||
| return { | ||
| latitude: ipGuess.latitude, | ||
| longitude: ipGuess.longitude, | ||
| timezone: ipGuess.timezone | ||
| }; | ||
| }; | ||
| var canPromptInteractively = () => Boolean( | ||
| process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true" | ||
| ); | ||
| var handleCancelledPrompt = () => { | ||
| p.cancel("Setup cancelled"); | ||
| return false; | ||
| }; | ||
| var runFirstRunSetup = async () => { | ||
| p.intro(ramadanGreen(`${MOON_EMOJI} Ramadan CLI Setup`)); | ||
| const ipSpinner = p.spinner(); | ||
| ipSpinner.start(`${MOON_EMOJI} Detecting your location...`); | ||
| const ipGuess = await guessLocation(); | ||
| ipSpinner.stop( | ||
| ipGuess ? `Detected: ${ipGuess.city}, ${ipGuess.country}` : "Could not detect location" | ||
| ); | ||
| const cityAnswer = await p.text({ | ||
| message: "Enter your city", | ||
| placeholder: "e.g., Lahore", | ||
| ...ipGuess?.city ? { | ||
| defaultValue: ipGuess.city, | ||
| initialValue: ipGuess.city | ||
| } : {}, | ||
| validate: (value) => { | ||
| if (!value.trim()) { | ||
| return "City is required."; | ||
| } | ||
| return void 0; | ||
| } | ||
| }); | ||
| if (p.isCancel(cityAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const city = toNonEmptyString(cityAnswer); | ||
| if (!city) { | ||
| p.log.error("Invalid city value."); | ||
| return false; | ||
| } | ||
| const countryAnswer = await p.text({ | ||
| message: "Enter your country", | ||
| placeholder: "e.g., Pakistan", | ||
| ...ipGuess?.country ? { | ||
| defaultValue: ipGuess.country, | ||
| initialValue: ipGuess.country | ||
| } : {}, | ||
| validate: (value) => { | ||
| if (!value.trim()) { | ||
| return "Country is required."; | ||
| } | ||
| return void 0; | ||
| } | ||
| }); | ||
| if (p.isCancel(countryAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const country = toNonEmptyString(countryAnswer); | ||
| if (!country) { | ||
| p.log.error("Invalid country value."); | ||
| return false; | ||
| } | ||
| const detailsSpinner = p.spinner(); | ||
| detailsSpinner.start(`${MOON_EMOJI} Resolving city details...`); | ||
| const detectedDetails = await resolveDetectedDetails(city, country, ipGuess); | ||
| detailsSpinner.stop( | ||
| detectedDetails.timezone ? `Detected timezone: ${detectedDetails.timezone}` : "Could not detect timezone for this city" | ||
| ); | ||
| const recommendedMethod = getRecommendedMethod(country); | ||
| const methodAnswer = await p.select({ | ||
| message: "Select calculation method", | ||
| initialValue: recommendedMethod ?? DEFAULT_METHOD2, | ||
| options: [...getMethodOptions(recommendedMethod)] | ||
| }); | ||
| if (p.isCancel(methodAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const method = toNumberSelection(methodAnswer); | ||
| if (method === null) { | ||
| p.log.error("Invalid method selection."); | ||
| return false; | ||
| } | ||
| const recommendedSchool = getRecommendedSchool(country); | ||
| const schoolAnswer = await p.select({ | ||
| message: "Select Asr school", | ||
| initialValue: recommendedSchool, | ||
| options: [...getSchoolOptions(recommendedSchool)] | ||
| }); | ||
| if (p.isCancel(schoolAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const school = toNumberSelection(schoolAnswer); | ||
| if (school === null) { | ||
| p.log.error("Invalid school selection."); | ||
| return false; | ||
| } | ||
| const hasDetectedTimezone = Boolean(detectedDetails.timezone); | ||
| const timezoneOptions = hasDetectedTimezone ? [ | ||
| { | ||
| value: "detected", | ||
| label: `Use detected timezone (${detectedDetails.timezone ?? ""})` | ||
| }, | ||
| { value: "custom", label: "Set custom timezone" }, | ||
| { value: "skip", label: "Do not set timezone override" } | ||
| ] : [ | ||
| { value: "custom", label: "Set custom timezone" }, | ||
| { value: "skip", label: "Do not set timezone override" } | ||
| ]; | ||
| const timezoneAnswer = await p.select({ | ||
| message: "Timezone preference", | ||
| initialValue: hasDetectedTimezone ? "detected" : "skip", | ||
| options: [...timezoneOptions] | ||
| }); | ||
| if (p.isCancel(timezoneAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const timezoneChoice = toTimezoneChoice(timezoneAnswer, hasDetectedTimezone); | ||
| if (!timezoneChoice) { | ||
| p.log.error("Invalid timezone selection."); | ||
| return false; | ||
| } | ||
| let timezone = timezoneChoice === "detected" ? detectedDetails.timezone : void 0; | ||
| if (timezoneChoice === "custom") { | ||
| const timezoneInput = await p.text({ | ||
| message: "Enter timezone", | ||
| placeholder: detectedDetails.timezone ?? "e.g., Asia/Karachi", | ||
| ...detectedDetails.timezone ? { | ||
| defaultValue: detectedDetails.timezone, | ||
| initialValue: detectedDetails.timezone | ||
| } : {}, | ||
| validate: (value) => { | ||
| if (!value.trim()) { | ||
| return "Timezone is required."; | ||
| } | ||
| return void 0; | ||
| } | ||
| }); | ||
| if (p.isCancel(timezoneInput)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const customTimezone = toNonEmptyString(timezoneInput); | ||
| if (!customTimezone) { | ||
| p.log.error("Invalid timezone value."); | ||
| return false; | ||
| } | ||
| timezone = customTimezone; | ||
| } | ||
| setStoredLocation({ | ||
| city, | ||
| country, | ||
| ...detectedDetails.latitude !== void 0 ? { latitude: detectedDetails.latitude } : {}, | ||
| ...detectedDetails.longitude !== void 0 ? { longitude: detectedDetails.longitude } : {} | ||
| }); | ||
| setStoredMethod(method); | ||
| setStoredSchool(school); | ||
| setStoredTimezone(timezone); | ||
| p.outro(ramadanGreen(`${MOON_EMOJI} Setup complete.`)); | ||
| return true; | ||
| }; | ||
| // src/ui/banner.ts | ||
| import pc3 from "picocolors"; | ||
| var ANSI_SHADOW = ` | ||
| \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 | ||
| \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 | ||
| \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 | ||
| \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 | ||
| \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 | ||
| \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D | ||
| `; | ||
| var ANSI_COMPACT = ` | ||
| \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 | ||
| \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 | ||
| \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 | ||
| \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 | ||
| \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 | ||
| `; | ||
| var tagLine = "Sehar \u2022 Iftar \u2022 Ramadan timings"; | ||
| var getBanner = () => { | ||
| const width = process.stdout.columns ?? 80; | ||
| const art = width >= 120 ? ANSI_SHADOW : ANSI_COMPACT; | ||
| const artColor = ramadanGreen(art.trimEnd()); | ||
| const lead = ramadanGreen(` ${MOON_EMOJI} Ramadan CLI`); | ||
| const tag = pc3.dim(` ${tagLine}`); | ||
| return ` | ||
| ${artColor} | ||
| ${lead} | ||
| ${tag} | ||
| `; | ||
| }; | ||
| // src/commands/ramadan.ts | ||
| var CITY_ALIAS_MAP = { | ||
| sf: "San Francisco" | ||
| }; | ||
| var normalizeCityAlias = (city) => { | ||
| const trimmed = city.trim(); | ||
| const alias = CITY_ALIAS_MAP[trimmed.toLowerCase()]; | ||
| if (!alias) { | ||
| return trimmed; | ||
| } | ||
| return alias; | ||
| }; | ||
| var to12HourTime = (value) => { | ||
| const cleanValue = value.split(" ")[0] ?? value; | ||
| const match = cleanValue.match(/^(\d{1,2}):(\d{2})$/); | ||
| if (!match) { | ||
| return cleanValue; | ||
| } | ||
| const hour = Number.parseInt(match[1] ?? "", 10); | ||
| const minute = Number.parseInt(match[2] ?? "", 10); | ||
| const isInvalidTime = Number.isNaN(hour) || Number.isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59; | ||
| if (isInvalidTime) { | ||
| return cleanValue; | ||
| } | ||
| const period = hour >= 12 ? "PM" : "AM"; | ||
| const twelveHour = hour % 12 || 12; | ||
| return `${twelveHour}:${String(minute).padStart(2, "0")} ${period}`; | ||
| }; | ||
| var toRamadanRow = (day, roza) => ({ | ||
| roza, | ||
| sehar: to12HourTime(day.timings.Fajr), | ||
| iftar: to12HourTime(day.timings.Maghrib), | ||
| date: day.date.readable, | ||
| hijri: `${day.date.hijri.day} ${day.date.hijri.month.en} ${day.date.hijri.year}` | ||
| }); | ||
| var getRozaNumberFromHijriDay = (day) => { | ||
| const parsed = Number.parseInt(day.date.hijri.day, 10); | ||
| if (Number.isNaN(parsed)) { | ||
| return 1; | ||
| } | ||
| return parsed; | ||
| }; | ||
| var DAY_MS = 24 * 60 * 60 * 1e3; | ||
| var MINUTES_IN_DAY = 24 * 60; | ||
| var parseIsoDate = (value) => { | ||
| const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| const year = Number.parseInt(match[1] ?? "", 10); | ||
| const month = Number.parseInt(match[2] ?? "", 10); | ||
| const day = Number.parseInt(match[3] ?? "", 10); | ||
| const date = new Date(year, month - 1, day); | ||
| const isValid = date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; | ||
| if (!isValid) { | ||
| return null; | ||
| } | ||
| return date; | ||
| }; | ||
| var parseGregorianDate = (value) => { | ||
| const match = value.match(/^(\d{2})-(\d{2})-(\d{4})$/); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| const day = Number.parseInt(match[1] ?? "", 10); | ||
| const month = Number.parseInt(match[2] ?? "", 10); | ||
| const year = Number.parseInt(match[3] ?? "", 10); | ||
| const date = new Date(year, month - 1, day); | ||
| const isValid = date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; | ||
| if (!isValid) { | ||
| return null; | ||
| } | ||
| return date; | ||
| }; | ||
| var addDays = (date, days) => { | ||
| const next = new Date(date); | ||
| next.setDate(next.getDate() + days); | ||
| return next; | ||
| }; | ||
| var toUtcDateOnlyMs = (date) => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()); | ||
| var getRozaNumberFromStartDate = (firstRozaDate, targetDate) => Math.floor( | ||
| (toUtcDateOnlyMs(targetDate) - toUtcDateOnlyMs(firstRozaDate)) / DAY_MS | ||
| ) + 1; | ||
| var parsePrayerTimeToMinutes = (value) => { | ||
| const cleanValue = value.split(" ")[0] ?? value; | ||
| const match = cleanValue.match(/^(\d{1,2}):(\d{2})$/); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| const hour = Number.parseInt(match[1] ?? "", 10); | ||
| const minute = Number.parseInt(match[2] ?? "", 10); | ||
| const isInvalid = Number.isNaN(hour) || Number.isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59; | ||
| if (isInvalid) { | ||
| return null; | ||
| } | ||
| return hour * 60 + minute; | ||
| }; | ||
| var parseGregorianDay = (value) => { | ||
| const match = value.match(/^(\d{2})-(\d{2})-(\d{4})$/); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| const day = Number.parseInt(match[1] ?? "", 10); | ||
| const month = Number.parseInt(match[2] ?? "", 10); | ||
| const year = Number.parseInt(match[3] ?? "", 10); | ||
| const isInvalid = Number.isNaN(day) || Number.isNaN(month) || Number.isNaN(year) || day < 1 || day > 31 || month < 1 || month > 12; | ||
| if (isInvalid) { | ||
| return null; | ||
| } | ||
| return { year, month, day }; | ||
| }; | ||
| var nowInTimezoneParts = (timezone) => { | ||
| try { | ||
| const formatter = new Intl.DateTimeFormat("en-GB", { | ||
| timeZone: timezone, | ||
| year: "numeric", | ||
| month: "2-digit", | ||
| day: "2-digit", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| hour12: false | ||
| }); | ||
| const parts = formatter.formatToParts(/* @__PURE__ */ new Date()); | ||
| const toNumber = (type) => { | ||
| const part = parts.find((item) => item.type === type)?.value; | ||
| if (!part) { | ||
| return null; | ||
| } | ||
| const parsed = Number.parseInt(part, 10); | ||
| if (Number.isNaN(parsed)) { | ||
| return null; | ||
| } | ||
| return parsed; | ||
| }; | ||
| const year = toNumber("year"); | ||
| const month = toNumber("month"); | ||
| const day = toNumber("day"); | ||
| let hour = toNumber("hour"); | ||
| const minute = toNumber("minute"); | ||
| if (year === null || month === null || day === null || hour === null || minute === null) { | ||
| return null; | ||
| } | ||
| if (hour === 24) { | ||
| hour = 0; | ||
| } | ||
| return { | ||
| year, | ||
| month, | ||
| day, | ||
| minutes: hour * 60 + minute | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| var formatCountdown = (minutes) => { | ||
| const safeMinutes = Math.max(minutes, 0); | ||
| const hours = Math.floor(safeMinutes / 60); | ||
| const remainingMinutes = safeMinutes % 60; | ||
| if (hours === 0) { | ||
| return `${remainingMinutes}m`; | ||
| } | ||
| return `${hours}h ${remainingMinutes}m`; | ||
| }; | ||
| var getHighlightState = (day) => { | ||
| const dayParts = parseGregorianDay(day.date.gregorian.date); | ||
| if (!dayParts) { | ||
| return null; | ||
| } | ||
| const seharMinutes = parsePrayerTimeToMinutes(day.timings.Fajr); | ||
| const iftarMinutes = parsePrayerTimeToMinutes(day.timings.Maghrib); | ||
| if (seharMinutes === null || iftarMinutes === null) { | ||
| return null; | ||
| } | ||
| const nowParts = nowInTimezoneParts(day.meta.timezone); | ||
| if (!nowParts) { | ||
| return null; | ||
| } | ||
| const nowDateUtc = Date.UTC(nowParts.year, nowParts.month - 1, nowParts.day); | ||
| const targetDateUtc = Date.UTC( | ||
| dayParts.year, | ||
| dayParts.month - 1, | ||
| dayParts.day | ||
| ); | ||
| const dayDiff = Math.floor((targetDateUtc - nowDateUtc) / DAY_MS); | ||
| if (dayDiff > 0) { | ||
| const minutesUntilSehar = dayDiff * MINUTES_IN_DAY + (seharMinutes - nowParts.minutes); | ||
| return { | ||
| current: "Before roza day", | ||
| next: "First Sehar", | ||
| countdown: formatCountdown(minutesUntilSehar) | ||
| }; | ||
| } | ||
| if (dayDiff < 0) { | ||
| return null; | ||
| } | ||
| if (nowParts.minutes < seharMinutes) { | ||
| return { | ||
| current: "Sehar window open", | ||
| next: "Roza starts (Fajr)", | ||
| countdown: formatCountdown(seharMinutes - nowParts.minutes) | ||
| }; | ||
| } | ||
| if (nowParts.minutes < iftarMinutes) { | ||
| return { | ||
| current: "Roza in progress", | ||
| next: "Iftar", | ||
| countdown: formatCountdown(iftarMinutes - nowParts.minutes) | ||
| }; | ||
| } | ||
| const minutesUntilNextSehar = MINUTES_IN_DAY - nowParts.minutes + seharMinutes; | ||
| return { | ||
| current: "Iftar time", | ||
| next: "Next day Sehar", | ||
| countdown: formatCountdown(minutesUntilNextSehar) | ||
| }; | ||
| }; | ||
| var getConfiguredFirstRozaDate = (opts) => { | ||
| if (opts.clearFirstRozaDate) { | ||
| clearStoredFirstRozaDate(); | ||
| return null; | ||
| } | ||
| if (opts.firstRozaDate) { | ||
| const parsedExplicit = parseIsoDate(opts.firstRozaDate); | ||
| if (!parsedExplicit) { | ||
| throw new Error("Invalid first roza date. Use YYYY-MM-DD."); | ||
| } | ||
| setStoredFirstRozaDate(opts.firstRozaDate); | ||
| return parsedExplicit; | ||
| } | ||
| const storedDate = getStoredFirstRozaDate(); | ||
| if (!storedDate) { | ||
| return null; | ||
| } | ||
| const parsedStored = parseIsoDate(storedDate); | ||
| if (parsedStored) { | ||
| return parsedStored; | ||
| } | ||
| clearStoredFirstRozaDate(); | ||
| return null; | ||
| }; | ||
| var getTargetRamadanYear = (today) => { | ||
| const hijriYear = Number.parseInt(today.date.hijri.year, 10); | ||
| const hijriMonth = today.date.hijri.month.number; | ||
| if (hijriMonth > 9) { | ||
| return hijriYear + 1; | ||
| } | ||
| return hijriYear; | ||
| }; | ||
| var formatRowAnnotation = (kind) => { | ||
| if (kind === "current") { | ||
| return pc4.green("\u2190 current"); | ||
| } | ||
| return pc4.yellow("\u2190 next"); | ||
| }; | ||
| var printTable = (rows, rowAnnotations = {}) => { | ||
| const headers = ["Roza", "Sehar", "Iftar", "Date", "Hijri"]; | ||
| const widths = [6, 8, 8, 14, 20]; | ||
| const pad = (value, index) => value.padEnd(widths[index] ?? value.length); | ||
| const line = (columns) => columns.map((column, index) => pad(column, index)).join(" "); | ||
| const divider = "-".repeat(line(headers).length); | ||
| console.log(pc4.dim(` ${line(headers)}`)); | ||
| console.log(pc4.dim(` ${divider}`)); | ||
| for (const row of rows) { | ||
| const rowLine = line([ | ||
| String(row.roza), | ||
| row.sehar, | ||
| row.iftar, | ||
| row.date, | ||
| row.hijri | ||
| ]); | ||
| const annotation = rowAnnotations[row.roza]; | ||
| if (!annotation) { | ||
| console.log(` ${rowLine}`); | ||
| continue; | ||
| } | ||
| console.log(` ${rowLine} ${formatRowAnnotation(annotation)}`); | ||
| } | ||
| }; | ||
| var getErrorMessage = (error) => { | ||
| if (error instanceof Error) { | ||
| return error.message; | ||
| } | ||
| return "unknown error"; | ||
| }; | ||
| var getJsonErrorCode = (message) => { | ||
| if (message.startsWith("Invalid first roza date")) { | ||
| return "INVALID_FIRST_ROZA_DATE"; | ||
| } | ||
| if (message.includes("Use either --all or --number")) { | ||
| return "INVALID_FLAG_COMBINATION"; | ||
| } | ||
| if (message.startsWith("Could not fetch prayer times.")) { | ||
| return "PRAYER_TIMES_FETCH_FAILED"; | ||
| } | ||
| if (message.startsWith("Could not fetch Ramadan calendar.")) { | ||
| return "RAMADAN_CALENDAR_FETCH_FAILED"; | ||
| } | ||
| if (message.startsWith("Could not detect location.")) { | ||
| return "LOCATION_DETECTION_FAILED"; | ||
| } | ||
| if (message.startsWith("Could not find roza")) { | ||
| return "ROZA_NOT_FOUND"; | ||
| } | ||
| if (message === "unknown error") { | ||
| return "UNKNOWN_ERROR"; | ||
| } | ||
| return "RAMADAN_CLI_ERROR"; | ||
| }; | ||
| var toJsonErrorPayload = (error) => { | ||
| const message = getErrorMessage(error); | ||
| return { | ||
| ok: false, | ||
| error: { | ||
| code: getJsonErrorCode(message), | ||
| message | ||
| } | ||
| }; | ||
| }; | ||
| var parseCityCountry = (value) => { | ||
| const parts = value.split(",").map((part) => part.trim()).filter(Boolean); | ||
| if (parts.length < 2) { | ||
| return null; | ||
| } | ||
| const city = normalizeCityAlias(parts[0] ?? ""); | ||
| if (!city) { | ||
| return null; | ||
| } | ||
| const country = parts.slice(1).join(", ").trim(); | ||
| if (!country) { | ||
| return null; | ||
| } | ||
| return { city, country }; | ||
| }; | ||
| var getAddressFromGuess = (guessed) => `${guessed.city}, ${guessed.country}`; | ||
| var withStoredSettings = (query) => { | ||
| const settings = getStoredPrayerSettings(); | ||
| const withMethodSchool = { | ||
| ...query, | ||
| method: settings.method, | ||
| school: settings.school | ||
| }; | ||
| if (!settings.timezone) { | ||
| return withMethodSchool; | ||
| } | ||
| return { | ||
| ...withMethodSchool, | ||
| timezone: settings.timezone | ||
| }; | ||
| }; | ||
| var withCountryAwareSettings = (query, country, cityTimezone) => { | ||
| const settings = getStoredPrayerSettings(); | ||
| let method = settings.method; | ||
| const recommendedMethod = getRecommendedMethod(country); | ||
| if (recommendedMethod !== null && shouldApplyRecommendedMethod(settings.method, recommendedMethod)) { | ||
| method = recommendedMethod; | ||
| } | ||
| let school = settings.school; | ||
| const recommendedSchool = getRecommendedSchool(country); | ||
| if (shouldApplyRecommendedSchool(settings.school, recommendedSchool)) { | ||
| school = recommendedSchool; | ||
| } | ||
| const timezone = cityTimezone ?? settings.timezone; | ||
| return { | ||
| ...query, | ||
| method, | ||
| school, | ||
| ...timezone ? { timezone } : {} | ||
| }; | ||
| }; | ||
| var getStoredQuery = () => { | ||
| if (!hasStoredLocation()) { | ||
| return null; | ||
| } | ||
| const location = getStoredLocation(); | ||
| if (location.city && location.country) { | ||
| const cityCountryQuery = { | ||
| address: `${location.city}, ${location.country}`, | ||
| city: location.city, | ||
| country: location.country, | ||
| ...location.latitude !== void 0 ? { latitude: location.latitude } : {}, | ||
| ...location.longitude !== void 0 ? { longitude: location.longitude } : {} | ||
| }; | ||
| return withStoredSettings({ | ||
| ...cityCountryQuery | ||
| }); | ||
| } | ||
| if (location.latitude !== void 0 && location.longitude !== void 0) { | ||
| return withStoredSettings({ | ||
| address: `${location.latitude}, ${location.longitude}`, | ||
| latitude: location.latitude, | ||
| longitude: location.longitude | ||
| }); | ||
| } | ||
| return null; | ||
| }; | ||
| var resolveQueryFromCityInput = async (city) => { | ||
| const normalizedInput = normalizeCityAlias(city); | ||
| const parsed = parseCityCountry(normalizedInput); | ||
| if (parsed) { | ||
| return withCountryAwareSettings( | ||
| { | ||
| address: `${parsed.city}, ${parsed.country}`, | ||
| city: parsed.city, | ||
| country: parsed.country | ||
| }, | ||
| parsed.country | ||
| ); | ||
| } | ||
| const guessed = await guessCityCountry(normalizedInput); | ||
| if (!guessed) { | ||
| return withStoredSettings({ address: normalizedInput }); | ||
| } | ||
| return withCountryAwareSettings( | ||
| { | ||
| address: `${guessed.city}, ${guessed.country}`, | ||
| city: guessed.city, | ||
| country: guessed.country, | ||
| latitude: guessed.latitude, | ||
| longitude: guessed.longitude | ||
| }, | ||
| guessed.country, | ||
| guessed.timezone | ||
| ); | ||
| }; | ||
| var resolveQuery = async (opts) => { | ||
| const city = opts.city; | ||
| if (city) { | ||
| return await resolveQueryFromCityInput(city); | ||
| } | ||
| const storedQuery = getStoredQuery(); | ||
| if (storedQuery) { | ||
| return storedQuery; | ||
| } | ||
| if (opts.allowInteractiveSetup && canPromptInteractively()) { | ||
| const configured = await runFirstRunSetup(); | ||
| if (!configured) { | ||
| process.exit(0); | ||
| } | ||
| const configuredQuery = getStoredQuery(); | ||
| if (configuredQuery) { | ||
| return configuredQuery; | ||
| } | ||
| } | ||
| const guessed = await guessLocation(); | ||
| if (!guessed) { | ||
| throw new Error( | ||
| 'Could not detect location. Pass a city like `ramadan-cli "Lahore"`.' | ||
| ); | ||
| } | ||
| saveAutoDetectedSetup(guessed); | ||
| return withStoredSettings({ | ||
| address: getAddressFromGuess(guessed), | ||
| city: guessed.city, | ||
| country: guessed.country, | ||
| latitude: guessed.latitude, | ||
| longitude: guessed.longitude | ||
| }); | ||
| }; | ||
| var fetchRamadanDay = async (query, date) => { | ||
| const errors = []; | ||
| const addressOptions = { | ||
| address: query.address | ||
| }; | ||
| if (query.method !== void 0) { | ||
| addressOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| addressOptions.school = query.school; | ||
| } | ||
| if (date) { | ||
| addressOptions.date = date; | ||
| } | ||
| try { | ||
| return await fetchTimingsByAddress(addressOptions); | ||
| } catch (error) { | ||
| errors.push(`timingsByAddress failed: ${getErrorMessage(error)}`); | ||
| } | ||
| if (query.city && query.country) { | ||
| const cityOptions = { | ||
| city: query.city, | ||
| country: query.country | ||
| }; | ||
| if (query.method !== void 0) { | ||
| cityOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| cityOptions.school = query.school; | ||
| } | ||
| if (date) { | ||
| cityOptions.date = date; | ||
| } | ||
| try { | ||
| return await fetchTimingsByCity(cityOptions); | ||
| } catch (error) { | ||
| errors.push(`timingsByCity failed: ${getErrorMessage(error)}`); | ||
| } | ||
| } | ||
| if (query.latitude !== void 0 && query.longitude !== void 0) { | ||
| const coordsOptions = { | ||
| latitude: query.latitude, | ||
| longitude: query.longitude | ||
| }; | ||
| if (query.method !== void 0) { | ||
| coordsOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| coordsOptions.school = query.school; | ||
| } | ||
| if (query.timezone) { | ||
| coordsOptions.timezone = query.timezone; | ||
| } | ||
| if (date) { | ||
| coordsOptions.date = date; | ||
| } | ||
| try { | ||
| return await fetchTimingsByCoords(coordsOptions); | ||
| } catch (error) { | ||
| errors.push(`timingsByCoords failed: ${getErrorMessage(error)}`); | ||
| } | ||
| } | ||
| throw new Error(`Could not fetch prayer times. ${errors.join(" | ")}`); | ||
| }; | ||
| var fetchRamadanCalendar = async (query, year) => { | ||
| const errors = []; | ||
| const addressOptions = { | ||
| address: query.address, | ||
| year, | ||
| month: 9 | ||
| }; | ||
| if (query.method !== void 0) { | ||
| addressOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| addressOptions.school = query.school; | ||
| } | ||
| try { | ||
| return await fetchHijriCalendarByAddress(addressOptions); | ||
| } catch (error) { | ||
| errors.push(`hijriCalendarByAddress failed: ${getErrorMessage(error)}`); | ||
| } | ||
| if (query.city && query.country) { | ||
| const cityOptions = { | ||
| city: query.city, | ||
| country: query.country, | ||
| year, | ||
| month: 9 | ||
| }; | ||
| if (query.method !== void 0) { | ||
| cityOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| cityOptions.school = query.school; | ||
| } | ||
| try { | ||
| return await fetchHijriCalendarByCity(cityOptions); | ||
| } catch (error) { | ||
| errors.push(`hijriCalendarByCity failed: ${getErrorMessage(error)}`); | ||
| } | ||
| } | ||
| throw new Error(`Could not fetch Ramadan calendar. ${errors.join(" | ")}`); | ||
| }; | ||
| var fetchCustomRamadanDays = async (query, firstRozaDate) => { | ||
| const totalDays = 30; | ||
| const days = Array.from( | ||
| { length: totalDays }, | ||
| (_, index) => addDays(firstRozaDate, index) | ||
| ); | ||
| return Promise.all( | ||
| days.map(async (dayDate) => fetchRamadanDay(query, dayDate)) | ||
| ); | ||
| }; | ||
| var getRowByRozaNumber = (days, rozaNumber) => { | ||
| const day = days[rozaNumber - 1]; | ||
| if (!day) { | ||
| throw new Error(`Could not find roza ${rozaNumber} timings.`); | ||
| } | ||
| return toRamadanRow(day, rozaNumber); | ||
| }; | ||
| var getDayByRozaNumber = (days, rozaNumber) => { | ||
| const day = days[rozaNumber - 1]; | ||
| if (!day) { | ||
| throw new Error(`Could not find roza ${rozaNumber} timings.`); | ||
| } | ||
| return day; | ||
| }; | ||
| var getHijriYearFromRozaNumber = (days, rozaNumber, fallbackYear) => { | ||
| const day = days[rozaNumber - 1]; | ||
| if (!day) { | ||
| return fallbackYear; | ||
| } | ||
| return Number.parseInt(day.date.hijri.year, 10); | ||
| }; | ||
| var setRowAnnotation = (annotations, roza, kind) => { | ||
| if (roza < 1 || roza > 30) { | ||
| return; | ||
| } | ||
| annotations[roza] = kind; | ||
| }; | ||
| var getAllModeRowAnnotations = (input) => { | ||
| const annotations = {}; | ||
| if (input.configuredFirstRozaDate) { | ||
| const currentRoza2 = getRozaNumberFromStartDate( | ||
| input.configuredFirstRozaDate, | ||
| input.todayGregorianDate | ||
| ); | ||
| if (currentRoza2 < 1) { | ||
| setRowAnnotation(annotations, 1, "next"); | ||
| return annotations; | ||
| } | ||
| setRowAnnotation(annotations, currentRoza2, "current"); | ||
| setRowAnnotation(annotations, currentRoza2 + 1, "next"); | ||
| return annotations; | ||
| } | ||
| const todayHijriYear = Number.parseInt(input.today.date.hijri.year, 10); | ||
| const isRamadanNow = input.today.date.hijri.month.number === 9 && todayHijriYear === input.targetYear; | ||
| if (!isRamadanNow) { | ||
| setRowAnnotation(annotations, 1, "next"); | ||
| return annotations; | ||
| } | ||
| const currentRoza = getRozaNumberFromHijriDay(input.today); | ||
| setRowAnnotation(annotations, currentRoza, "current"); | ||
| setRowAnnotation(annotations, currentRoza + 1, "next"); | ||
| return annotations; | ||
| }; | ||
| var printTextOutput = (output, plain, highlight, rowAnnotations = {}) => { | ||
| const title = output.mode === "all" ? `Ramadan ${output.hijriYear} (All Days)` : output.mode === "number" ? `Roza ${output.rows[0]?.roza ?? ""} Sehar/Iftar` : "Today Sehar/Iftar"; | ||
| console.log(plain ? "RAMADAN CLI" : getBanner()); | ||
| console.log(ramadanGreen(` ${title}`)); | ||
| console.log(pc4.dim(` \u{1F4CD} ${output.location}`)); | ||
| console.log(""); | ||
| printTable(output.rows, rowAnnotations); | ||
| console.log(""); | ||
| if (highlight) { | ||
| console.log(` ${ramadanGreen("Status:")} ${pc4.white(highlight.current)}`); | ||
| console.log( | ||
| ` ${ramadanGreen("Up next:")} ${pc4.white(highlight.next)} in ${pc4.yellow(highlight.countdown)}` | ||
| ); | ||
| console.log(""); | ||
| } | ||
| console.log(pc4.dim(" Sehar uses Fajr. Iftar uses Maghrib.")); | ||
| console.log(""); | ||
| }; | ||
| var formatStatusLine = (highlight) => { | ||
| const label = (() => { | ||
| switch (highlight.next) { | ||
| case "First Sehar": | ||
| case "Next day Sehar": | ||
| return "Sehar"; | ||
| case "Roza starts (Fajr)": | ||
| return "Fast starts"; | ||
| default: | ||
| return highlight.next; | ||
| } | ||
| })(); | ||
| return `${label} in ${highlight.countdown}`; | ||
| }; | ||
| var ramadanCommand = async (opts) => { | ||
| const isSilent = opts.json || opts.status; | ||
| const spinner2 = isSilent ? null : ora({ | ||
| text: "Fetching Ramadan timings...", | ||
| stream: process.stdout | ||
| }); | ||
| if (opts.status) { | ||
| try { | ||
| const query = await resolveQuery({ | ||
| city: opts.city, | ||
| allowInteractiveSetup: false | ||
| }); | ||
| const today = await fetchRamadanDay(query); | ||
| const highlight = getHighlightState(today); | ||
| if (highlight) { | ||
| console.log(formatStatusLine(highlight)); | ||
| } | ||
| } catch { | ||
| } | ||
| return; | ||
| } | ||
| try { | ||
| const configuredFirstRozaDate = getConfiguredFirstRozaDate(opts); | ||
| const query = await resolveQuery({ | ||
| city: opts.city, | ||
| allowInteractiveSetup: !opts.json | ||
| }); | ||
| spinner2?.start(); | ||
| const today = await fetchRamadanDay(query); | ||
| const todayGregorianDate = parseGregorianDate(today.date.gregorian.date); | ||
| if (!todayGregorianDate) { | ||
| throw new Error("Could not parse Gregorian date from prayer response."); | ||
| } | ||
| const targetYear = getTargetRamadanYear(today); | ||
| const hasCustomFirstRozaDate = configuredFirstRozaDate !== null; | ||
| if (opts.all && opts.rozaNumber !== void 0) { | ||
| throw new Error("Use either --all or --number, not both."); | ||
| } | ||
| if (opts.rozaNumber !== void 0) { | ||
| let row; | ||
| let hijriYear2 = targetYear; | ||
| let selectedDay; | ||
| if (hasCustomFirstRozaDate) { | ||
| const firstRozaDate = configuredFirstRozaDate; | ||
| if (!firstRozaDate) { | ||
| throw new Error("Could not determine first roza date."); | ||
| } | ||
| const customDays = await fetchCustomRamadanDays(query, firstRozaDate); | ||
| row = getRowByRozaNumber(customDays, opts.rozaNumber); | ||
| selectedDay = getDayByRozaNumber(customDays, opts.rozaNumber); | ||
| hijriYear2 = getHijriYearFromRozaNumber( | ||
| customDays, | ||
| opts.rozaNumber, | ||
| targetYear | ||
| ); | ||
| } else { | ||
| const calendar = await fetchRamadanCalendar(query, targetYear); | ||
| row = getRowByRozaNumber(calendar, opts.rozaNumber); | ||
| selectedDay = getDayByRozaNumber(calendar, opts.rozaNumber); | ||
| hijriYear2 = getHijriYearFromRozaNumber( | ||
| calendar, | ||
| opts.rozaNumber, | ||
| targetYear | ||
| ); | ||
| } | ||
| const output2 = { | ||
| mode: "number", | ||
| location: query.address, | ||
| hijriYear: hijriYear2, | ||
| rows: [row] | ||
| }; | ||
| spinner2?.stop(); | ||
| if (opts.json) { | ||
| console.log(JSON.stringify(output2, null, 2)); | ||
| return; | ||
| } | ||
| printTextOutput( | ||
| output2, | ||
| Boolean(opts.plain), | ||
| getHighlightState(selectedDay) | ||
| ); | ||
| return; | ||
| } | ||
| if (!opts.all) { | ||
| let row = null; | ||
| let outputHijriYear = targetYear; | ||
| let highlightDay = null; | ||
| if (hasCustomFirstRozaDate) { | ||
| const firstRozaDate = configuredFirstRozaDate; | ||
| if (!firstRozaDate) { | ||
| throw new Error("Could not determine first roza date."); | ||
| } | ||
| const rozaNumber = getRozaNumberFromStartDate( | ||
| firstRozaDate, | ||
| todayGregorianDate | ||
| ); | ||
| if (rozaNumber < 1) { | ||
| const firstRozaDay = await fetchRamadanDay(query, firstRozaDate); | ||
| row = toRamadanRow(firstRozaDay, 1); | ||
| highlightDay = firstRozaDay; | ||
| outputHijriYear = Number.parseInt(firstRozaDay.date.hijri.year, 10); | ||
| } | ||
| if (rozaNumber >= 1) { | ||
| row = toRamadanRow(today, rozaNumber); | ||
| highlightDay = today; | ||
| outputHijriYear = Number.parseInt(today.date.hijri.year, 10); | ||
| } | ||
| } | ||
| if (!hasCustomFirstRozaDate) { | ||
| const isRamadanNow = today.date.hijri.month.number === 9; | ||
| if (isRamadanNow) { | ||
| row = toRamadanRow(today, getRozaNumberFromHijriDay(today)); | ||
| highlightDay = today; | ||
| } | ||
| if (!isRamadanNow) { | ||
| const calendar = await fetchRamadanCalendar(query, targetYear); | ||
| const firstRamadanDay = calendar[0]; | ||
| if (!firstRamadanDay) { | ||
| throw new Error("Could not find the first day of Ramadan."); | ||
| } | ||
| row = toRamadanRow(firstRamadanDay, 1); | ||
| highlightDay = firstRamadanDay; | ||
| outputHijriYear = Number.parseInt( | ||
| firstRamadanDay.date.hijri.year, | ||
| 10 | ||
| ); | ||
| } | ||
| } | ||
| if (!row) { | ||
| throw new Error("Could not determine roza number."); | ||
| } | ||
| const output2 = { | ||
| mode: "today", | ||
| location: query.address, | ||
| hijriYear: outputHijriYear, | ||
| rows: [row] | ||
| }; | ||
| spinner2?.stop(); | ||
| if (opts.json) { | ||
| console.log(JSON.stringify(output2, null, 2)); | ||
| return; | ||
| } | ||
| printTextOutput( | ||
| output2, | ||
| Boolean(opts.plain), | ||
| getHighlightState(highlightDay ?? today) | ||
| ); | ||
| return; | ||
| } | ||
| let rows = []; | ||
| let hijriYear = targetYear; | ||
| if (hasCustomFirstRozaDate) { | ||
| const firstRozaDate = configuredFirstRozaDate; | ||
| if (!firstRozaDate) { | ||
| throw new Error("Could not determine first roza date."); | ||
| } | ||
| const customDays = await fetchCustomRamadanDays(query, firstRozaDate); | ||
| rows = customDays.map((day, index) => toRamadanRow(day, index + 1)); | ||
| const firstCustomDay = customDays[0]; | ||
| if (firstCustomDay) { | ||
| hijriYear = Number.parseInt(firstCustomDay.date.hijri.year, 10); | ||
| } | ||
| } | ||
| if (!hasCustomFirstRozaDate) { | ||
| const calendar = await fetchRamadanCalendar(query, targetYear); | ||
| rows = calendar.map((day, index) => toRamadanRow(day, index + 1)); | ||
| } | ||
| const output = { | ||
| mode: "all", | ||
| location: query.address, | ||
| hijriYear, | ||
| rows | ||
| }; | ||
| const allModeRowAnnotations = getAllModeRowAnnotations({ | ||
| today, | ||
| todayGregorianDate, | ||
| targetYear, | ||
| configuredFirstRozaDate | ||
| }); | ||
| spinner2?.stop(); | ||
| if (opts.json) { | ||
| console.log(JSON.stringify(output, null, 2)); | ||
| return; | ||
| } | ||
| printTextOutput( | ||
| output, | ||
| Boolean(opts.plain), | ||
| getHighlightState(today), | ||
| allModeRowAnnotations | ||
| ); | ||
| } catch (error) { | ||
| if (opts.json) { | ||
| process.stderr.write(`${JSON.stringify(toJsonErrorPayload(error))} | ||
| `); | ||
| process.exit(1); | ||
| } | ||
| spinner2?.fail( | ||
| error instanceof Error ? error.message : "Failed to fetch Ramadan timings" | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| }; | ||
| export { | ||
| getRecommendedMethod, | ||
| getRecommendedSchool, | ||
| shouldApplyRecommendedMethod, | ||
| shouldApplyRecommendedSchool, | ||
| getStoredLocation, | ||
| hasStoredLocation, | ||
| getStoredPrayerSettings, | ||
| setStoredLocation, | ||
| setStoredTimezone, | ||
| setStoredMethod, | ||
| setStoredSchool, | ||
| getStoredFirstRozaDate, | ||
| setStoredFirstRozaDate, | ||
| clearStoredFirstRozaDate, | ||
| clearRamadanConfig, | ||
| saveAutoDetectedSetup, | ||
| applyRecommendedSettingsIfUnset, | ||
| configCommand, | ||
| fetchTimingsByCity, | ||
| fetchTimingsByAddress, | ||
| fetchTimingsByCoords, | ||
| fetchNextPrayer, | ||
| fetchCalendarByCity, | ||
| fetchCalendarByAddress, | ||
| fetchHijriCalendarByAddress, | ||
| fetchHijriCalendarByCity, | ||
| fetchMethods, | ||
| fetchQibla, | ||
| guessLocation, | ||
| guessCityCountry, | ||
| ramadanCommand | ||
| }; |
+3
-2
@@ -6,3 +6,3 @@ #!/usr/bin/env node | ||
| ramadanCommand | ||
| } from "./chunk-ZE6KRFJM.js"; | ||
| } from "./chunk-MFEFTDJ3.js"; | ||
@@ -26,3 +26,3 @@ // src/cli.ts | ||
| parseRozaNumber | ||
| ).option("-p, --plain", "Plain text output").option("-j, --json", "JSON output").option( | ||
| ).option("-p, --plain", "Plain text output").option("-j, --json", "JSON output").option("-s, --status", "Status line output (next event only, for status bars)").option( | ||
| "--first-roza-date <YYYY-MM-DD>", | ||
@@ -40,2 +40,3 @@ "Set and use a custom first roza date" | ||
| json: opts.json, | ||
| status: opts.status, | ||
| firstRozaDate: opts.firstRozaDate, | ||
@@ -42,0 +43,0 @@ clearFirstRozaDate: opts.clearFirstRozaDate |
+1
-0
@@ -209,2 +209,3 @@ type MethodId = number & { | ||
| readonly json?: boolean | undefined; | ||
| readonly status?: boolean | undefined; | ||
| readonly firstRozaDate?: string | undefined; | ||
@@ -211,0 +212,0 @@ readonly clearFirstRozaDate?: boolean | undefined; |
+1
-1
@@ -34,3 +34,3 @@ #!/usr/bin/env node | ||
| shouldApplyRecommendedSchool | ||
| } from "./chunk-ZE6KRFJM.js"; | ||
| } from "./chunk-MFEFTDJ3.js"; | ||
| export { | ||
@@ -37,0 +37,0 @@ applyRecommendedSettingsIfUnset, |
+1
-1
| { | ||
| "name": "ramadan-cli", | ||
| "version": "6.0.1", | ||
| "version": "6.1.0", | ||
| "description": "Ramadan CLI with automatic location detection and Sehar/Iftar timings", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+8
-1
@@ -23,2 +23,3 @@ [](https://x.com/MrAhmadAwais/) | ||
| - 🔢 `-n, --number` for a specific roza day | ||
| - 📟 `-s, --status` single-line next event for status bars and coding agents | ||
| - 🧪 Custom first roza override (`--first-roza-date`) | ||
@@ -70,2 +71,7 @@ - 🧹 One-command reset (`reset`) | ||
| # Status line (next event only — for status bars, coding agents). | ||
| roza -s | ||
| roza --status | ||
| roza -s --city Lahore | ||
| # Set custom first roza date (stored). | ||
@@ -117,2 +123,3 @@ roza --first-roza-date 2026-02-19 | ||
| | `-j, --json` | `boolean` | `false` | JSON-only output for scripts | | ||
| | `-s, --status` | `boolean` | `false` | Single-line next event output for status bars and coding agents | | ||
| | `--first-roza-date <YYYY-MM-DD>` | `string` | stored/API | Persist custom first roza date | | ||
@@ -168,3 +175,3 @@ | `--clear-first-roza-date` | `boolean` | `false` | Clear custom first roza date and use API Ramadan date | | ||
| - On first run (TTY), CLI launches interactive setup with Clack prompts. | ||
| - If `--json` is used and no config exists, interactive setup is skipped. | ||
| - If `--json` or `--status` is used and no config exists, interactive setup is skipped. | ||
| - Config changes are explicit via `config`, `reset`, and first-roza flags. | ||
@@ -171,0 +178,0 @@ - No stdin input contract yet. Input is args/flags only. |
| #!/usr/bin/env node | ||
| // src/recommendations.ts | ||
| var countryMethodMap = { | ||
| "United States": 2, | ||
| USA: 2, | ||
| US: 2, | ||
| Canada: 2, | ||
| Mexico: 2, | ||
| Pakistan: 1, | ||
| Bangladesh: 1, | ||
| India: 1, | ||
| Afghanistan: 1, | ||
| "United Kingdom": 3, | ||
| UK: 3, | ||
| Germany: 3, | ||
| Netherlands: 3, | ||
| Belgium: 3, | ||
| Sweden: 3, | ||
| Norway: 3, | ||
| Denmark: 3, | ||
| Finland: 3, | ||
| Austria: 3, | ||
| Switzerland: 3, | ||
| Poland: 3, | ||
| Italy: 3, | ||
| Spain: 3, | ||
| Greece: 3, | ||
| Japan: 3, | ||
| China: 3, | ||
| "South Korea": 3, | ||
| Australia: 3, | ||
| "New Zealand": 3, | ||
| "South Africa": 3, | ||
| "Saudi Arabia": 4, | ||
| Yemen: 4, | ||
| Oman: 4, | ||
| Bahrain: 4, | ||
| Egypt: 5, | ||
| Syria: 5, | ||
| Lebanon: 5, | ||
| Palestine: 5, | ||
| Jordan: 5, | ||
| Iraq: 5, | ||
| Libya: 5, | ||
| Sudan: 5, | ||
| Iran: 7, | ||
| Kuwait: 9, | ||
| Qatar: 10, | ||
| Singapore: 11, | ||
| France: 12, | ||
| Turkey: 13, | ||
| T\u00FCrkiye: 13, | ||
| Russia: 14, | ||
| "United Arab Emirates": 16, | ||
| UAE: 16, | ||
| Malaysia: 17, | ||
| Brunei: 17, | ||
| Tunisia: 18, | ||
| Algeria: 19, | ||
| Indonesia: 20, | ||
| Morocco: 21, | ||
| Portugal: 22 | ||
| }; | ||
| var hanafiCountries = /* @__PURE__ */ new Set([ | ||
| "Pakistan", | ||
| "Bangladesh", | ||
| "India", | ||
| "Afghanistan", | ||
| "Turkey", | ||
| "T\xFCrkiye", | ||
| "Iraq", | ||
| "Syria", | ||
| "Jordan", | ||
| "Palestine", | ||
| "Kazakhstan", | ||
| "Uzbekistan", | ||
| "Tajikistan", | ||
| "Turkmenistan", | ||
| "Kyrgyzstan" | ||
| ]); | ||
| var getRecommendedMethod = (country) => { | ||
| const direct = countryMethodMap[country]; | ||
| if (direct !== void 0) { | ||
| return direct; | ||
| } | ||
| const lowerCountry = country.toLowerCase(); | ||
| for (const [key, value] of Object.entries(countryMethodMap)) { | ||
| if (key.toLowerCase() === lowerCountry) { | ||
| return value; | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
| var getRecommendedSchool = (country) => { | ||
| if (hanafiCountries.has(country)) { | ||
| return 1; | ||
| } | ||
| const lowerCountry = country.toLowerCase(); | ||
| for (const listedCountry of hanafiCountries) { | ||
| if (listedCountry.toLowerCase() === lowerCountry) { | ||
| return 1; | ||
| } | ||
| } | ||
| return 0; | ||
| }; | ||
| // src/ramadan-config.ts | ||
| import Conf from "conf"; | ||
| import { z } from "zod"; | ||
| var SharedConfigSchema = z.object({ | ||
| latitude: z.number().optional(), | ||
| longitude: z.number().optional(), | ||
| city: z.string().optional(), | ||
| country: z.string().optional(), | ||
| method: z.number().optional(), | ||
| school: z.number().optional(), | ||
| timezone: z.string().optional(), | ||
| firstRozaDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), | ||
| format24h: z.boolean().optional() | ||
| }); | ||
| var IsoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/); | ||
| var DEFAULT_METHOD = 2; | ||
| var DEFAULT_SCHOOL = 0; | ||
| var getConfigCwd = () => { | ||
| const configuredPath = process.env.RAMADAN_CLI_CONFIG_DIR; | ||
| if (configuredPath) { | ||
| return configuredPath; | ||
| } | ||
| const isTestRuntime = process.env.VITEST === "true" || process.env.NODE_ENV === "test"; | ||
| if (isTestRuntime) { | ||
| return "/tmp"; | ||
| } | ||
| return void 0; | ||
| }; | ||
| var shouldApplyRecommendedMethod = (currentMethod, recommendedMethod) => currentMethod === DEFAULT_METHOD || currentMethod === recommendedMethod; | ||
| var shouldApplyRecommendedSchool = (currentSchool, recommendedSchool) => currentSchool === DEFAULT_SCHOOL || currentSchool === recommendedSchool; | ||
| var configCwd = getConfigCwd(); | ||
| var sharedConfig = new Conf({ | ||
| projectName: "ramadan-cli", | ||
| ...configCwd ? { cwd: configCwd } : {}, | ||
| defaults: { | ||
| method: DEFAULT_METHOD, | ||
| school: DEFAULT_SCHOOL, | ||
| format24h: false | ||
| } | ||
| }); | ||
| var legacyAzaanConfig = new Conf({ | ||
| projectName: "azaan", | ||
| ...configCwd ? { cwd: configCwd } : {} | ||
| }); | ||
| var getValidatedStore = () => { | ||
| const parsed = SharedConfigSchema.safeParse(sharedConfig.store); | ||
| if (!parsed.success) { | ||
| return { | ||
| method: DEFAULT_METHOD, | ||
| school: DEFAULT_SCHOOL, | ||
| format24h: false | ||
| }; | ||
| } | ||
| return parsed.data; | ||
| }; | ||
| var getStoredLocation = () => { | ||
| const store = getValidatedStore(); | ||
| return { | ||
| city: store.city, | ||
| country: store.country, | ||
| latitude: store.latitude, | ||
| longitude: store.longitude | ||
| }; | ||
| }; | ||
| var hasStoredLocation = () => { | ||
| const location = getStoredLocation(); | ||
| const hasCityCountry = Boolean(location.city && location.country); | ||
| const hasCoords = Boolean( | ||
| location.latitude !== void 0 && location.longitude !== void 0 | ||
| ); | ||
| return hasCityCountry || hasCoords; | ||
| }; | ||
| var getStoredPrayerSettings = () => { | ||
| const store = getValidatedStore(); | ||
| return { | ||
| method: store.method ?? DEFAULT_METHOD, | ||
| school: store.school ?? DEFAULT_SCHOOL, | ||
| timezone: store.timezone | ||
| }; | ||
| }; | ||
| var setStoredLocation = (location) => { | ||
| if (location.city) { | ||
| sharedConfig.set("city", location.city); | ||
| } | ||
| if (location.country) { | ||
| sharedConfig.set("country", location.country); | ||
| } | ||
| if (location.latitude !== void 0) { | ||
| sharedConfig.set("latitude", location.latitude); | ||
| } | ||
| if (location.longitude !== void 0) { | ||
| sharedConfig.set("longitude", location.longitude); | ||
| } | ||
| }; | ||
| var setStoredTimezone = (timezone) => { | ||
| if (!timezone) { | ||
| return; | ||
| } | ||
| sharedConfig.set("timezone", timezone); | ||
| }; | ||
| var setStoredMethod = (method) => { | ||
| sharedConfig.set("method", method); | ||
| }; | ||
| var setStoredSchool = (school) => { | ||
| sharedConfig.set("school", school); | ||
| }; | ||
| var getStoredFirstRozaDate = () => { | ||
| const store = getValidatedStore(); | ||
| return store.firstRozaDate; | ||
| }; | ||
| var setStoredFirstRozaDate = (firstRozaDate) => { | ||
| const parsed = IsoDateSchema.safeParse(firstRozaDate); | ||
| if (!parsed.success) { | ||
| throw new Error("Invalid first roza date. Use YYYY-MM-DD."); | ||
| } | ||
| sharedConfig.set("firstRozaDate", parsed.data); | ||
| }; | ||
| var clearStoredFirstRozaDate = () => { | ||
| sharedConfig.delete("firstRozaDate"); | ||
| }; | ||
| var clearRamadanConfig = () => { | ||
| sharedConfig.clear(); | ||
| legacyAzaanConfig.clear(); | ||
| }; | ||
| var maybeSetRecommendedMethod = (country) => { | ||
| const recommendedMethod = getRecommendedMethod(country); | ||
| if (recommendedMethod === null) { | ||
| return; | ||
| } | ||
| const currentMethod = sharedConfig.get("method") ?? DEFAULT_METHOD; | ||
| if (!shouldApplyRecommendedMethod(currentMethod, recommendedMethod)) { | ||
| return; | ||
| } | ||
| sharedConfig.set("method", recommendedMethod); | ||
| }; | ||
| var maybeSetRecommendedSchool = (country) => { | ||
| const currentSchool = sharedConfig.get("school") ?? DEFAULT_SCHOOL; | ||
| const recommendedSchool = getRecommendedSchool(country); | ||
| if (!shouldApplyRecommendedSchool(currentSchool, recommendedSchool)) { | ||
| return; | ||
| } | ||
| sharedConfig.set("school", recommendedSchool); | ||
| }; | ||
| var saveAutoDetectedSetup = (location) => { | ||
| setStoredLocation({ | ||
| city: location.city, | ||
| country: location.country, | ||
| latitude: location.latitude, | ||
| longitude: location.longitude | ||
| }); | ||
| setStoredTimezone(location.timezone); | ||
| maybeSetRecommendedMethod(location.country); | ||
| maybeSetRecommendedSchool(location.country); | ||
| }; | ||
| var applyRecommendedSettingsIfUnset = (country) => { | ||
| maybeSetRecommendedMethod(country); | ||
| maybeSetRecommendedSchool(country); | ||
| }; | ||
| // src/commands/config.ts | ||
| import pc from "picocolors"; | ||
| import { z as z2 } from "zod"; | ||
| var MethodSchema = z2.coerce.number().int().min(0).max(23); | ||
| var SchoolSchema = z2.coerce.number().int().min(0).max(1); | ||
| var LatitudeSchema = z2.coerce.number().min(-90).max(90); | ||
| var LongitudeSchema = z2.coerce.number().min(-180).max(180); | ||
| var parseOptionalWithSchema = (value, schema, label) => { | ||
| if (value === void 0) { | ||
| return void 0; | ||
| } | ||
| const parsed = schema.safeParse(value); | ||
| if (!parsed.success) { | ||
| throw new Error(`Invalid ${label}.`); | ||
| } | ||
| return parsed.data; | ||
| }; | ||
| var parseConfigUpdates = (options) => ({ | ||
| ...options.city ? { city: options.city.trim() } : {}, | ||
| ...options.country ? { country: options.country.trim() } : {}, | ||
| ...options.latitude !== void 0 ? { | ||
| latitude: parseOptionalWithSchema( | ||
| options.latitude, | ||
| LatitudeSchema, | ||
| "latitude" | ||
| ) | ||
| } : {}, | ||
| ...options.longitude !== void 0 ? { | ||
| longitude: parseOptionalWithSchema( | ||
| options.longitude, | ||
| LongitudeSchema, | ||
| "longitude" | ||
| ) | ||
| } : {}, | ||
| ...options.method !== void 0 ? { | ||
| method: parseOptionalWithSchema(options.method, MethodSchema, "method") | ||
| } : {}, | ||
| ...options.school !== void 0 ? { | ||
| school: parseOptionalWithSchema(options.school, SchoolSchema, "school") | ||
| } : {}, | ||
| ...options.timezone ? { timezone: options.timezone.trim() } : {} | ||
| }); | ||
| var mergeLocationUpdates = (current, updates) => ({ | ||
| ...updates.city !== void 0 ? { city: updates.city } : current.city ? { city: current.city } : {}, | ||
| ...updates.country !== void 0 ? { country: updates.country } : current.country ? { country: current.country } : {}, | ||
| ...updates.latitude !== void 0 ? { latitude: updates.latitude } : current.latitude !== void 0 ? { latitude: current.latitude } : {}, | ||
| ...updates.longitude !== void 0 ? { longitude: updates.longitude } : current.longitude !== void 0 ? { longitude: current.longitude } : {} | ||
| }); | ||
| var printCurrentConfig = () => { | ||
| const location = getStoredLocation(); | ||
| const settings = getStoredPrayerSettings(); | ||
| const firstRozaDate = getStoredFirstRozaDate(); | ||
| console.log(pc.dim("Current configuration:")); | ||
| if (location.city) { | ||
| console.log(` City: ${location.city}`); | ||
| } | ||
| if (location.country) { | ||
| console.log(` Country: ${location.country}`); | ||
| } | ||
| if (location.latitude !== void 0) { | ||
| console.log(` Latitude: ${location.latitude}`); | ||
| } | ||
| if (location.longitude !== void 0) { | ||
| console.log(` Longitude: ${location.longitude}`); | ||
| } | ||
| console.log(` Method: ${settings.method}`); | ||
| console.log(` School: ${settings.school}`); | ||
| if (settings.timezone) { | ||
| console.log(` Timezone: ${settings.timezone}`); | ||
| } | ||
| if (firstRozaDate) { | ||
| console.log(` First Roza Date: ${firstRozaDate}`); | ||
| } | ||
| }; | ||
| var hasConfigUpdateFlags = (options) => Boolean( | ||
| options.city || options.country || options.latitude !== void 0 || options.longitude !== void 0 || options.method !== void 0 || options.school !== void 0 || options.timezone | ||
| ); | ||
| var configCommand = async (options) => { | ||
| if (options.clear) { | ||
| clearRamadanConfig(); | ||
| console.log(pc.green("Configuration cleared.")); | ||
| return; | ||
| } | ||
| if (options.show) { | ||
| printCurrentConfig(); | ||
| return; | ||
| } | ||
| if (!hasConfigUpdateFlags(options)) { | ||
| console.log( | ||
| pc.dim( | ||
| "No config updates provided. Use `ramadan-cli config --show` to inspect." | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const updates = parseConfigUpdates(options); | ||
| const currentLocation = getStoredLocation(); | ||
| const nextLocation = mergeLocationUpdates(currentLocation, updates); | ||
| setStoredLocation(nextLocation); | ||
| if (updates.method !== void 0) { | ||
| setStoredMethod(updates.method); | ||
| } | ||
| if (updates.school !== void 0) { | ||
| setStoredSchool(updates.school); | ||
| } | ||
| if (updates.timezone) { | ||
| setStoredTimezone(updates.timezone); | ||
| } | ||
| console.log(pc.green("Configuration updated.")); | ||
| }; | ||
| // src/api.ts | ||
| import { z as z3 } from "zod"; | ||
| var API_BASE = "https://api.aladhan.com/v1"; | ||
| var PrayerTimingsSchema = z3.object({ | ||
| Fajr: z3.string(), | ||
| Sunrise: z3.string(), | ||
| Dhuhr: z3.string(), | ||
| Asr: z3.string(), | ||
| Sunset: z3.string(), | ||
| Maghrib: z3.string(), | ||
| Isha: z3.string(), | ||
| Imsak: z3.string(), | ||
| Midnight: z3.string(), | ||
| Firstthird: z3.string(), | ||
| Lastthird: z3.string() | ||
| }); | ||
| var HijriDateSchema = z3.object({ | ||
| date: z3.string(), | ||
| day: z3.string(), | ||
| month: z3.object({ | ||
| number: z3.number(), | ||
| en: z3.string(), | ||
| ar: z3.string() | ||
| }), | ||
| year: z3.string(), | ||
| weekday: z3.object({ | ||
| en: z3.string(), | ||
| ar: z3.string() | ||
| }) | ||
| }); | ||
| var GregorianDateSchema = z3.object({ | ||
| date: z3.string(), | ||
| day: z3.string(), | ||
| month: z3.object({ | ||
| number: z3.number(), | ||
| en: z3.string() | ||
| }), | ||
| year: z3.string(), | ||
| weekday: z3.object({ | ||
| en: z3.string() | ||
| }) | ||
| }); | ||
| var PrayerMetaSchema = z3.object({ | ||
| latitude: z3.number(), | ||
| longitude: z3.number(), | ||
| timezone: z3.string(), | ||
| method: z3.object({ | ||
| id: z3.number(), | ||
| name: z3.string() | ||
| }), | ||
| school: z3.union([ | ||
| z3.object({ | ||
| id: z3.number(), | ||
| name: z3.string() | ||
| }), | ||
| z3.string() | ||
| ]) | ||
| }); | ||
| var PrayerDataSchema = z3.object({ | ||
| timings: PrayerTimingsSchema, | ||
| date: z3.object({ | ||
| readable: z3.string(), | ||
| timestamp: z3.string(), | ||
| hijri: HijriDateSchema, | ||
| gregorian: GregorianDateSchema | ||
| }), | ||
| meta: PrayerMetaSchema | ||
| }); | ||
| var NextPrayerDataSchema = z3.object({ | ||
| timings: PrayerTimingsSchema, | ||
| date: z3.object({ | ||
| readable: z3.string(), | ||
| timestamp: z3.string(), | ||
| hijri: HijriDateSchema, | ||
| gregorian: GregorianDateSchema | ||
| }), | ||
| meta: PrayerMetaSchema, | ||
| nextPrayer: z3.string(), | ||
| nextPrayerTime: z3.string() | ||
| }); | ||
| var CalculationMethodSchema = z3.object({ | ||
| id: z3.number(), | ||
| name: z3.string(), | ||
| params: z3.object({ | ||
| Fajr: z3.number(), | ||
| Isha: z3.union([z3.number(), z3.string()]) | ||
| }) | ||
| }); | ||
| var QiblaDataSchema = z3.object({ | ||
| latitude: z3.number(), | ||
| longitude: z3.number(), | ||
| direction: z3.number() | ||
| }); | ||
| var ApiEnvelopeSchema = z3.object({ | ||
| code: z3.number(), | ||
| status: z3.string(), | ||
| data: z3.unknown() | ||
| }); | ||
| var formatDate = (date) => { | ||
| const day = String(date.getDate()).padStart(2, "0"); | ||
| const month = String(date.getMonth() + 1).padStart(2, "0"); | ||
| const year = date.getFullYear(); | ||
| return `${day}-${month}-${year}`; | ||
| }; | ||
| var parseApiResponse = (payload, dataSchema) => { | ||
| const parsedEnvelope = ApiEnvelopeSchema.safeParse(payload); | ||
| if (!parsedEnvelope.success) { | ||
| throw new Error( | ||
| `Invalid API response: ${parsedEnvelope.error.issues[0]?.message ?? "Unknown schema mismatch"}` | ||
| ); | ||
| } | ||
| if (parsedEnvelope.data.code !== 200) { | ||
| throw new Error( | ||
| `API ${parsedEnvelope.data.code}: ${parsedEnvelope.data.status}` | ||
| ); | ||
| } | ||
| if (typeof parsedEnvelope.data.data === "string") { | ||
| throw new Error(`API returned message: ${parsedEnvelope.data.data}`); | ||
| } | ||
| const parsedData = dataSchema.safeParse(parsedEnvelope.data.data); | ||
| if (!parsedData.success) { | ||
| throw new Error( | ||
| `Invalid API response: ${parsedData.error.issues[0]?.message ?? "Unknown schema mismatch"}` | ||
| ); | ||
| } | ||
| return parsedData.data; | ||
| }; | ||
| var fetchAndParse = async (url, dataSchema) => { | ||
| const response = await fetch(url); | ||
| const json = await response.json(); | ||
| return parseApiResponse(json, dataSchema); | ||
| }; | ||
| var fetchTimingsByCity = async (opts) => { | ||
| const date = formatDate(opts.date ?? /* @__PURE__ */ new Date()); | ||
| const params = new URLSearchParams({ | ||
| city: opts.city, | ||
| country: opts.country | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/timingsByCity/${date}?${params}`, | ||
| PrayerDataSchema | ||
| ); | ||
| }; | ||
| var fetchTimingsByAddress = async (opts) => { | ||
| const date = formatDate(opts.date ?? /* @__PURE__ */ new Date()); | ||
| const params = new URLSearchParams({ | ||
| address: opts.address | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/timingsByAddress/${date}?${params}`, | ||
| PrayerDataSchema | ||
| ); | ||
| }; | ||
| var fetchTimingsByCoords = async (opts) => { | ||
| const date = formatDate(opts.date ?? /* @__PURE__ */ new Date()); | ||
| const params = new URLSearchParams({ | ||
| latitude: String(opts.latitude), | ||
| longitude: String(opts.longitude) | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| if (opts.timezone) { | ||
| params.set("timezonestring", opts.timezone); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/timings/${date}?${params}`, | ||
| PrayerDataSchema | ||
| ); | ||
| }; | ||
| var fetchNextPrayer = async (opts) => { | ||
| const date = formatDate(/* @__PURE__ */ new Date()); | ||
| const params = new URLSearchParams({ | ||
| latitude: String(opts.latitude), | ||
| longitude: String(opts.longitude) | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| if (opts.timezone) { | ||
| params.set("timezonestring", opts.timezone); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/nextPrayer/${date}?${params}`, | ||
| NextPrayerDataSchema | ||
| ); | ||
| }; | ||
| var fetchCalendarByCity = async (opts) => { | ||
| const params = new URLSearchParams({ | ||
| city: opts.city, | ||
| country: opts.country | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| const path = opts.month ? `${opts.year}/${opts.month}` : String(opts.year); | ||
| return fetchAndParse( | ||
| `${API_BASE}/calendarByCity/${path}?${params}`, | ||
| z3.array(PrayerDataSchema) | ||
| ); | ||
| }; | ||
| var fetchCalendarByAddress = async (opts) => { | ||
| const params = new URLSearchParams({ | ||
| address: opts.address | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| const path = opts.month ? `${opts.year}/${opts.month}` : String(opts.year); | ||
| return fetchAndParse( | ||
| `${API_BASE}/calendarByAddress/${path}?${params}`, | ||
| z3.array(PrayerDataSchema) | ||
| ); | ||
| }; | ||
| var fetchHijriCalendarByAddress = async (opts) => { | ||
| const params = new URLSearchParams({ | ||
| address: opts.address | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/hijriCalendarByAddress/${opts.year}/${opts.month}?${params}`, | ||
| z3.array(PrayerDataSchema) | ||
| ); | ||
| }; | ||
| var fetchHijriCalendarByCity = async (opts) => { | ||
| const params = new URLSearchParams({ | ||
| city: opts.city, | ||
| country: opts.country | ||
| }); | ||
| if (opts.method !== void 0) { | ||
| params.set("method", String(opts.method)); | ||
| } | ||
| if (opts.school !== void 0) { | ||
| params.set("school", String(opts.school)); | ||
| } | ||
| return fetchAndParse( | ||
| `${API_BASE}/hijriCalendarByCity/${opts.year}/${opts.month}?${params}`, | ||
| z3.array(PrayerDataSchema) | ||
| ); | ||
| }; | ||
| var fetchMethods = async () => fetchAndParse( | ||
| `${API_BASE}/methods`, | ||
| z3.record(z3.string(), CalculationMethodSchema) | ||
| ); | ||
| var fetchQibla = async (latitude, longitude) => fetchAndParse(`${API_BASE}/qibla/${latitude}/${longitude}`, QiblaDataSchema); | ||
| // src/geo.ts | ||
| import { z as z4 } from "zod"; | ||
| var IpApiSchema = z4.object({ | ||
| city: z4.string(), | ||
| country: z4.string(), | ||
| lat: z4.number(), | ||
| lon: z4.number(), | ||
| timezone: z4.string().optional() | ||
| }); | ||
| var IpapiCoSchema = z4.object({ | ||
| city: z4.string(), | ||
| country_name: z4.string(), | ||
| latitude: z4.number(), | ||
| longitude: z4.number(), | ||
| timezone: z4.string().optional() | ||
| }); | ||
| var IpWhoisSchema = z4.object({ | ||
| success: z4.boolean(), | ||
| city: z4.string(), | ||
| country: z4.string(), | ||
| latitude: z4.number(), | ||
| longitude: z4.number(), | ||
| timezone: z4.object({ | ||
| id: z4.string().optional() | ||
| }).optional() | ||
| }); | ||
| var OpenMeteoSearchSchema = z4.object({ | ||
| results: z4.array( | ||
| z4.object({ | ||
| name: z4.string(), | ||
| country: z4.string(), | ||
| latitude: z4.number(), | ||
| longitude: z4.number(), | ||
| timezone: z4.string().optional() | ||
| }) | ||
| ).optional() | ||
| }); | ||
| var fetchJson = async (url) => { | ||
| const response = await fetch(url); | ||
| return await response.json(); | ||
| }; | ||
| var tryIpApi = async () => { | ||
| try { | ||
| const json = await fetchJson( | ||
| "http://ip-api.com/json/?fields=city,country,lat,lon,timezone" | ||
| ); | ||
| const parsed = IpApiSchema.safeParse(json); | ||
| if (!parsed.success) { | ||
| return null; | ||
| } | ||
| return { | ||
| city: parsed.data.city, | ||
| country: parsed.data.country, | ||
| latitude: parsed.data.lat, | ||
| longitude: parsed.data.lon, | ||
| timezone: parsed.data.timezone ?? "" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| var tryIpapiCo = async () => { | ||
| try { | ||
| const json = await fetchJson("https://ipapi.co/json/"); | ||
| const parsed = IpapiCoSchema.safeParse(json); | ||
| if (!parsed.success) { | ||
| return null; | ||
| } | ||
| return { | ||
| city: parsed.data.city, | ||
| country: parsed.data.country_name, | ||
| latitude: parsed.data.latitude, | ||
| longitude: parsed.data.longitude, | ||
| timezone: parsed.data.timezone ?? "" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| var tryIpWhois = async () => { | ||
| try { | ||
| const json = await fetchJson("https://ipwho.is/"); | ||
| const parsed = IpWhoisSchema.safeParse(json); | ||
| if (!parsed.success) { | ||
| return null; | ||
| } | ||
| if (!parsed.data.success) { | ||
| return null; | ||
| } | ||
| return { | ||
| city: parsed.data.city, | ||
| country: parsed.data.country, | ||
| latitude: parsed.data.latitude, | ||
| longitude: parsed.data.longitude, | ||
| timezone: parsed.data.timezone?.id ?? "" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| var guessLocation = async () => { | ||
| const fromIpApi = await tryIpApi(); | ||
| if (fromIpApi) { | ||
| return fromIpApi; | ||
| } | ||
| const fromIpapi = await tryIpapiCo(); | ||
| if (fromIpapi) { | ||
| return fromIpapi; | ||
| } | ||
| return tryIpWhois(); | ||
| }; | ||
| var guessCityCountry = async (query) => { | ||
| const trimmedQuery = query.trim(); | ||
| if (!trimmedQuery) { | ||
| return null; | ||
| } | ||
| try { | ||
| const url = new URL("https://geocoding-api.open-meteo.com/v1/search"); | ||
| url.searchParams.set("name", trimmedQuery); | ||
| url.searchParams.set("count", "1"); | ||
| url.searchParams.set("language", "en"); | ||
| url.searchParams.set("format", "json"); | ||
| const json = await fetchJson(url.toString()); | ||
| const parsed = OpenMeteoSearchSchema.safeParse(json); | ||
| if (!parsed.success) { | ||
| return null; | ||
| } | ||
| const result = parsed.data.results?.[0]; | ||
| if (!result) { | ||
| return null; | ||
| } | ||
| return { | ||
| city: result.name, | ||
| country: result.country, | ||
| latitude: result.latitude, | ||
| longitude: result.longitude, | ||
| ...result.timezone ? { timezone: result.timezone } : {} | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| // src/commands/ramadan.ts | ||
| import ora from "ora"; | ||
| import pc4 from "picocolors"; | ||
| // src/setup.ts | ||
| import * as p from "@clack/prompts"; | ||
| // src/ui/theme.ts | ||
| import pc2 from "picocolors"; | ||
| var MOON_EMOJI = "\u{1F319}"; | ||
| var RAMADAN_GREEN_RGB = "38;2;128;240;151"; | ||
| var ANSI_RESET = "\x1B[0m"; | ||
| var supportsTrueColor = () => { | ||
| const colorTerm = process.env.COLORTERM?.toLowerCase() ?? ""; | ||
| return colorTerm.includes("truecolor") || colorTerm.includes("24bit"); | ||
| }; | ||
| var ramadanGreen = (value) => { | ||
| if (!pc2.isColorSupported) { | ||
| return value; | ||
| } | ||
| if (!supportsTrueColor()) { | ||
| return pc2.green(value); | ||
| } | ||
| return `\x1B[${RAMADAN_GREEN_RGB}m${value}${ANSI_RESET}`; | ||
| }; | ||
| // src/setup.ts | ||
| var METHOD_OPTIONS = [ | ||
| { value: 0, label: "Jafari (Shia Ithna-Ashari)" }, | ||
| { value: 1, label: "Karachi (Pakistan)" }, | ||
| { value: 2, label: "ISNA (North America)" }, | ||
| { value: 3, label: "MWL (Muslim World League)" }, | ||
| { value: 4, label: "Makkah (Umm al-Qura)" }, | ||
| { value: 5, label: "Egypt" }, | ||
| { value: 7, label: "Tehran (Shia)" }, | ||
| { value: 8, label: "Gulf Region" }, | ||
| { value: 9, label: "Kuwait" }, | ||
| { value: 10, label: "Qatar" }, | ||
| { value: 11, label: "Singapore" }, | ||
| { value: 12, label: "France" }, | ||
| { value: 13, label: "Turkey" }, | ||
| { value: 14, label: "Russia" }, | ||
| { value: 15, label: "Moonsighting Committee" }, | ||
| { value: 16, label: "Dubai" }, | ||
| { value: 17, label: "Malaysia (JAKIM)" }, | ||
| { value: 18, label: "Tunisia" }, | ||
| { value: 19, label: "Algeria" }, | ||
| { value: 20, label: "Indonesia" }, | ||
| { value: 21, label: "Morocco" }, | ||
| { value: 22, label: "Portugal" }, | ||
| { value: 23, label: "Jordan" } | ||
| ]; | ||
| var SCHOOL_SHAFI = 0; | ||
| var SCHOOL_HANAFI = 1; | ||
| var DEFAULT_METHOD2 = 2; | ||
| var normalize = (value) => value.trim().toLowerCase(); | ||
| var toNonEmptyString = (value) => { | ||
| if (typeof value !== "string") { | ||
| return null; | ||
| } | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| return null; | ||
| } | ||
| return trimmed; | ||
| }; | ||
| var toNumberSelection = (value) => { | ||
| if (typeof value !== "number") { | ||
| return null; | ||
| } | ||
| return value; | ||
| }; | ||
| var toTimezoneChoice = (value, hasDetectedOption) => { | ||
| if (value === "custom") { | ||
| return "custom"; | ||
| } | ||
| if (value === "skip") { | ||
| return "skip"; | ||
| } | ||
| if (hasDetectedOption && value === "detected") { | ||
| return "detected"; | ||
| } | ||
| return null; | ||
| }; | ||
| var findMethodLabel = (method) => { | ||
| const option = METHOD_OPTIONS.find((entry) => entry.value === method); | ||
| if (option) { | ||
| return option.label; | ||
| } | ||
| return `Method ${method}`; | ||
| }; | ||
| var getMethodOptions = (recommendedMethod) => { | ||
| if (recommendedMethod === null) { | ||
| return METHOD_OPTIONS; | ||
| } | ||
| const recommendedOption = { | ||
| value: recommendedMethod, | ||
| label: `${findMethodLabel(recommendedMethod)} (Recommended)`, | ||
| hint: "Based on your country" | ||
| }; | ||
| const remaining = METHOD_OPTIONS.filter( | ||
| (option) => option.value !== recommendedMethod | ||
| ); | ||
| return [recommendedOption, ...remaining]; | ||
| }; | ||
| var getSchoolOptions = (recommendedSchool) => { | ||
| if (recommendedSchool === SCHOOL_HANAFI) { | ||
| return [ | ||
| { | ||
| value: SCHOOL_HANAFI, | ||
| label: "Hanafi (Recommended)", | ||
| hint: "Later Asr timing" | ||
| }, | ||
| { | ||
| value: SCHOOL_SHAFI, | ||
| label: "Shafi", | ||
| hint: "Standard Asr timing" | ||
| } | ||
| ]; | ||
| } | ||
| return [ | ||
| { | ||
| value: SCHOOL_SHAFI, | ||
| label: "Shafi (Recommended)", | ||
| hint: "Standard Asr timing" | ||
| }, | ||
| { | ||
| value: SCHOOL_HANAFI, | ||
| label: "Hanafi", | ||
| hint: "Later Asr timing" | ||
| } | ||
| ]; | ||
| }; | ||
| var cityCountryMatchesGuess = (city, country, guess) => normalize(city) === normalize(guess.city) && normalize(country) === normalize(guess.country); | ||
| var resolveDetectedDetails = async (city, country, ipGuess) => { | ||
| const geocoded = await guessCityCountry(`${city}, ${country}`); | ||
| if (geocoded) { | ||
| return { | ||
| latitude: geocoded.latitude, | ||
| longitude: geocoded.longitude, | ||
| timezone: geocoded.timezone | ||
| }; | ||
| } | ||
| if (!ipGuess) { | ||
| return {}; | ||
| } | ||
| if (!cityCountryMatchesGuess(city, country, ipGuess)) { | ||
| return {}; | ||
| } | ||
| return { | ||
| latitude: ipGuess.latitude, | ||
| longitude: ipGuess.longitude, | ||
| timezone: ipGuess.timezone | ||
| }; | ||
| }; | ||
| var canPromptInteractively = () => Boolean( | ||
| process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true" | ||
| ); | ||
| var handleCancelledPrompt = () => { | ||
| p.cancel("Setup cancelled"); | ||
| return false; | ||
| }; | ||
| var runFirstRunSetup = async () => { | ||
| p.intro(ramadanGreen(`${MOON_EMOJI} Ramadan CLI Setup`)); | ||
| const ipSpinner = p.spinner(); | ||
| ipSpinner.start(`${MOON_EMOJI} Detecting your location...`); | ||
| const ipGuess = await guessLocation(); | ||
| ipSpinner.stop( | ||
| ipGuess ? `Detected: ${ipGuess.city}, ${ipGuess.country}` : "Could not detect location" | ||
| ); | ||
| const cityAnswer = await p.text({ | ||
| message: "Enter your city", | ||
| placeholder: "e.g., Lahore", | ||
| ...ipGuess?.city ? { | ||
| defaultValue: ipGuess.city, | ||
| initialValue: ipGuess.city | ||
| } : {}, | ||
| validate: (value) => { | ||
| if (!value.trim()) { | ||
| return "City is required."; | ||
| } | ||
| return void 0; | ||
| } | ||
| }); | ||
| if (p.isCancel(cityAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const city = toNonEmptyString(cityAnswer); | ||
| if (!city) { | ||
| p.log.error("Invalid city value."); | ||
| return false; | ||
| } | ||
| const countryAnswer = await p.text({ | ||
| message: "Enter your country", | ||
| placeholder: "e.g., Pakistan", | ||
| ...ipGuess?.country ? { | ||
| defaultValue: ipGuess.country, | ||
| initialValue: ipGuess.country | ||
| } : {}, | ||
| validate: (value) => { | ||
| if (!value.trim()) { | ||
| return "Country is required."; | ||
| } | ||
| return void 0; | ||
| } | ||
| }); | ||
| if (p.isCancel(countryAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const country = toNonEmptyString(countryAnswer); | ||
| if (!country) { | ||
| p.log.error("Invalid country value."); | ||
| return false; | ||
| } | ||
| const detailsSpinner = p.spinner(); | ||
| detailsSpinner.start(`${MOON_EMOJI} Resolving city details...`); | ||
| const detectedDetails = await resolveDetectedDetails(city, country, ipGuess); | ||
| detailsSpinner.stop( | ||
| detectedDetails.timezone ? `Detected timezone: ${detectedDetails.timezone}` : "Could not detect timezone for this city" | ||
| ); | ||
| const recommendedMethod = getRecommendedMethod(country); | ||
| const methodAnswer = await p.select({ | ||
| message: "Select calculation method", | ||
| initialValue: recommendedMethod ?? DEFAULT_METHOD2, | ||
| options: [...getMethodOptions(recommendedMethod)] | ||
| }); | ||
| if (p.isCancel(methodAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const method = toNumberSelection(methodAnswer); | ||
| if (method === null) { | ||
| p.log.error("Invalid method selection."); | ||
| return false; | ||
| } | ||
| const recommendedSchool = getRecommendedSchool(country); | ||
| const schoolAnswer = await p.select({ | ||
| message: "Select Asr school", | ||
| initialValue: recommendedSchool, | ||
| options: [...getSchoolOptions(recommendedSchool)] | ||
| }); | ||
| if (p.isCancel(schoolAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const school = toNumberSelection(schoolAnswer); | ||
| if (school === null) { | ||
| p.log.error("Invalid school selection."); | ||
| return false; | ||
| } | ||
| const hasDetectedTimezone = Boolean(detectedDetails.timezone); | ||
| const timezoneOptions = hasDetectedTimezone ? [ | ||
| { | ||
| value: "detected", | ||
| label: `Use detected timezone (${detectedDetails.timezone ?? ""})` | ||
| }, | ||
| { value: "custom", label: "Set custom timezone" }, | ||
| { value: "skip", label: "Do not set timezone override" } | ||
| ] : [ | ||
| { value: "custom", label: "Set custom timezone" }, | ||
| { value: "skip", label: "Do not set timezone override" } | ||
| ]; | ||
| const timezoneAnswer = await p.select({ | ||
| message: "Timezone preference", | ||
| initialValue: hasDetectedTimezone ? "detected" : "skip", | ||
| options: [...timezoneOptions] | ||
| }); | ||
| if (p.isCancel(timezoneAnswer)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const timezoneChoice = toTimezoneChoice(timezoneAnswer, hasDetectedTimezone); | ||
| if (!timezoneChoice) { | ||
| p.log.error("Invalid timezone selection."); | ||
| return false; | ||
| } | ||
| let timezone = timezoneChoice === "detected" ? detectedDetails.timezone : void 0; | ||
| if (timezoneChoice === "custom") { | ||
| const timezoneInput = await p.text({ | ||
| message: "Enter timezone", | ||
| placeholder: detectedDetails.timezone ?? "e.g., Asia/Karachi", | ||
| ...detectedDetails.timezone ? { | ||
| defaultValue: detectedDetails.timezone, | ||
| initialValue: detectedDetails.timezone | ||
| } : {}, | ||
| validate: (value) => { | ||
| if (!value.trim()) { | ||
| return "Timezone is required."; | ||
| } | ||
| return void 0; | ||
| } | ||
| }); | ||
| if (p.isCancel(timezoneInput)) { | ||
| return handleCancelledPrompt(); | ||
| } | ||
| const customTimezone = toNonEmptyString(timezoneInput); | ||
| if (!customTimezone) { | ||
| p.log.error("Invalid timezone value."); | ||
| return false; | ||
| } | ||
| timezone = customTimezone; | ||
| } | ||
| setStoredLocation({ | ||
| city, | ||
| country, | ||
| ...detectedDetails.latitude !== void 0 ? { latitude: detectedDetails.latitude } : {}, | ||
| ...detectedDetails.longitude !== void 0 ? { longitude: detectedDetails.longitude } : {} | ||
| }); | ||
| setStoredMethod(method); | ||
| setStoredSchool(school); | ||
| setStoredTimezone(timezone); | ||
| p.outro(ramadanGreen(`${MOON_EMOJI} Setup complete.`)); | ||
| return true; | ||
| }; | ||
| // src/ui/banner.ts | ||
| import pc3 from "picocolors"; | ||
| var ANSI_SHADOW = ` | ||
| \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 | ||
| \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 | ||
| \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 | ||
| \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 | ||
| \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 | ||
| \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D | ||
| `; | ||
| var ANSI_COMPACT = ` | ||
| \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 | ||
| \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 | ||
| \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 | ||
| \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 | ||
| \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 | ||
| `; | ||
| var tagLine = "Sehar \u2022 Iftar \u2022 Ramadan timings"; | ||
| var getBanner = () => { | ||
| const width = process.stdout.columns ?? 80; | ||
| const art = width >= 120 ? ANSI_SHADOW : ANSI_COMPACT; | ||
| const artColor = ramadanGreen(art.trimEnd()); | ||
| const lead = ramadanGreen(` ${MOON_EMOJI} Ramadan CLI`); | ||
| const tag = pc3.dim(` ${tagLine}`); | ||
| return ` | ||
| ${artColor} | ||
| ${lead} | ||
| ${tag} | ||
| `; | ||
| }; | ||
| // src/commands/ramadan.ts | ||
| var CITY_ALIAS_MAP = { | ||
| sf: "San Francisco" | ||
| }; | ||
| var normalizeCityAlias = (city) => { | ||
| const trimmed = city.trim(); | ||
| const alias = CITY_ALIAS_MAP[trimmed.toLowerCase()]; | ||
| if (!alias) { | ||
| return trimmed; | ||
| } | ||
| return alias; | ||
| }; | ||
| var to12HourTime = (value) => { | ||
| const cleanValue = value.split(" ")[0] ?? value; | ||
| const match = cleanValue.match(/^(\d{1,2}):(\d{2})$/); | ||
| if (!match) { | ||
| return cleanValue; | ||
| } | ||
| const hour = Number.parseInt(match[1] ?? "", 10); | ||
| const minute = Number.parseInt(match[2] ?? "", 10); | ||
| const isInvalidTime = Number.isNaN(hour) || Number.isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59; | ||
| if (isInvalidTime) { | ||
| return cleanValue; | ||
| } | ||
| const period = hour >= 12 ? "PM" : "AM"; | ||
| const twelveHour = hour % 12 || 12; | ||
| return `${twelveHour}:${String(minute).padStart(2, "0")} ${period}`; | ||
| }; | ||
| var toRamadanRow = (day, roza) => ({ | ||
| roza, | ||
| sehar: to12HourTime(day.timings.Fajr), | ||
| iftar: to12HourTime(day.timings.Maghrib), | ||
| date: day.date.readable, | ||
| hijri: `${day.date.hijri.day} ${day.date.hijri.month.en} ${day.date.hijri.year}` | ||
| }); | ||
| var getRozaNumberFromHijriDay = (day) => { | ||
| const parsed = Number.parseInt(day.date.hijri.day, 10); | ||
| if (Number.isNaN(parsed)) { | ||
| return 1; | ||
| } | ||
| return parsed; | ||
| }; | ||
| var DAY_MS = 24 * 60 * 60 * 1e3; | ||
| var MINUTES_IN_DAY = 24 * 60; | ||
| var parseIsoDate = (value) => { | ||
| const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| const year = Number.parseInt(match[1] ?? "", 10); | ||
| const month = Number.parseInt(match[2] ?? "", 10); | ||
| const day = Number.parseInt(match[3] ?? "", 10); | ||
| const date = new Date(year, month - 1, day); | ||
| const isValid = date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; | ||
| if (!isValid) { | ||
| return null; | ||
| } | ||
| return date; | ||
| }; | ||
| var parseGregorianDate = (value) => { | ||
| const match = value.match(/^(\d{2})-(\d{2})-(\d{4})$/); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| const day = Number.parseInt(match[1] ?? "", 10); | ||
| const month = Number.parseInt(match[2] ?? "", 10); | ||
| const year = Number.parseInt(match[3] ?? "", 10); | ||
| const date = new Date(year, month - 1, day); | ||
| const isValid = date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; | ||
| if (!isValid) { | ||
| return null; | ||
| } | ||
| return date; | ||
| }; | ||
| var addDays = (date, days) => { | ||
| const next = new Date(date); | ||
| next.setDate(next.getDate() + days); | ||
| return next; | ||
| }; | ||
| var toUtcDateOnlyMs = (date) => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()); | ||
| var getRozaNumberFromStartDate = (firstRozaDate, targetDate) => Math.floor( | ||
| (toUtcDateOnlyMs(targetDate) - toUtcDateOnlyMs(firstRozaDate)) / DAY_MS | ||
| ) + 1; | ||
| var parsePrayerTimeToMinutes = (value) => { | ||
| const cleanValue = value.split(" ")[0] ?? value; | ||
| const match = cleanValue.match(/^(\d{1,2}):(\d{2})$/); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| const hour = Number.parseInt(match[1] ?? "", 10); | ||
| const minute = Number.parseInt(match[2] ?? "", 10); | ||
| const isInvalid = Number.isNaN(hour) || Number.isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59; | ||
| if (isInvalid) { | ||
| return null; | ||
| } | ||
| return hour * 60 + minute; | ||
| }; | ||
| var parseGregorianDay = (value) => { | ||
| const match = value.match(/^(\d{2})-(\d{2})-(\d{4})$/); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| const day = Number.parseInt(match[1] ?? "", 10); | ||
| const month = Number.parseInt(match[2] ?? "", 10); | ||
| const year = Number.parseInt(match[3] ?? "", 10); | ||
| const isInvalid = Number.isNaN(day) || Number.isNaN(month) || Number.isNaN(year) || day < 1 || day > 31 || month < 1 || month > 12; | ||
| if (isInvalid) { | ||
| return null; | ||
| } | ||
| return { year, month, day }; | ||
| }; | ||
| var nowInTimezoneParts = (timezone) => { | ||
| try { | ||
| const formatter = new Intl.DateTimeFormat("en-GB", { | ||
| timeZone: timezone, | ||
| year: "numeric", | ||
| month: "2-digit", | ||
| day: "2-digit", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| hour12: false | ||
| }); | ||
| const parts = formatter.formatToParts(/* @__PURE__ */ new Date()); | ||
| const toNumber = (type) => { | ||
| const part = parts.find((item) => item.type === type)?.value; | ||
| if (!part) { | ||
| return null; | ||
| } | ||
| const parsed = Number.parseInt(part, 10); | ||
| if (Number.isNaN(parsed)) { | ||
| return null; | ||
| } | ||
| return parsed; | ||
| }; | ||
| const year = toNumber("year"); | ||
| const month = toNumber("month"); | ||
| const day = toNumber("day"); | ||
| let hour = toNumber("hour"); | ||
| const minute = toNumber("minute"); | ||
| if (year === null || month === null || day === null || hour === null || minute === null) { | ||
| return null; | ||
| } | ||
| if (hour === 24) { | ||
| hour = 0; | ||
| } | ||
| return { | ||
| year, | ||
| month, | ||
| day, | ||
| minutes: hour * 60 + minute | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
| var formatCountdown = (minutes) => { | ||
| const safeMinutes = Math.max(minutes, 0); | ||
| const hours = Math.floor(safeMinutes / 60); | ||
| const remainingMinutes = safeMinutes % 60; | ||
| if (hours === 0) { | ||
| return `${remainingMinutes}m`; | ||
| } | ||
| return `${hours}h ${remainingMinutes}m`; | ||
| }; | ||
| var getHighlightState = (day) => { | ||
| const dayParts = parseGregorianDay(day.date.gregorian.date); | ||
| if (!dayParts) { | ||
| return null; | ||
| } | ||
| const seharMinutes = parsePrayerTimeToMinutes(day.timings.Fajr); | ||
| const iftarMinutes = parsePrayerTimeToMinutes(day.timings.Maghrib); | ||
| if (seharMinutes === null || iftarMinutes === null) { | ||
| return null; | ||
| } | ||
| const nowParts = nowInTimezoneParts(day.meta.timezone); | ||
| if (!nowParts) { | ||
| return null; | ||
| } | ||
| const nowDateUtc = Date.UTC(nowParts.year, nowParts.month - 1, nowParts.day); | ||
| const targetDateUtc = Date.UTC( | ||
| dayParts.year, | ||
| dayParts.month - 1, | ||
| dayParts.day | ||
| ); | ||
| const dayDiff = Math.floor((targetDateUtc - nowDateUtc) / DAY_MS); | ||
| if (dayDiff > 0) { | ||
| const minutesUntilSehar = dayDiff * MINUTES_IN_DAY + (seharMinutes - nowParts.minutes); | ||
| return { | ||
| current: "Before roza day", | ||
| next: "First Sehar", | ||
| countdown: formatCountdown(minutesUntilSehar) | ||
| }; | ||
| } | ||
| if (dayDiff < 0) { | ||
| return null; | ||
| } | ||
| if (nowParts.minutes < seharMinutes) { | ||
| return { | ||
| current: "Sehar window open", | ||
| next: "Roza starts (Fajr)", | ||
| countdown: formatCountdown(seharMinutes - nowParts.minutes) | ||
| }; | ||
| } | ||
| if (nowParts.minutes < iftarMinutes) { | ||
| return { | ||
| current: "Roza in progress", | ||
| next: "Iftar", | ||
| countdown: formatCountdown(iftarMinutes - nowParts.minutes) | ||
| }; | ||
| } | ||
| const minutesUntilNextSehar = MINUTES_IN_DAY - nowParts.minutes + seharMinutes; | ||
| return { | ||
| current: "Iftar time", | ||
| next: "Next day Sehar", | ||
| countdown: formatCountdown(minutesUntilNextSehar) | ||
| }; | ||
| }; | ||
| var getConfiguredFirstRozaDate = (opts) => { | ||
| if (opts.clearFirstRozaDate) { | ||
| clearStoredFirstRozaDate(); | ||
| return null; | ||
| } | ||
| if (opts.firstRozaDate) { | ||
| const parsedExplicit = parseIsoDate(opts.firstRozaDate); | ||
| if (!parsedExplicit) { | ||
| throw new Error("Invalid first roza date. Use YYYY-MM-DD."); | ||
| } | ||
| setStoredFirstRozaDate(opts.firstRozaDate); | ||
| return parsedExplicit; | ||
| } | ||
| const storedDate = getStoredFirstRozaDate(); | ||
| if (!storedDate) { | ||
| return null; | ||
| } | ||
| const parsedStored = parseIsoDate(storedDate); | ||
| if (parsedStored) { | ||
| return parsedStored; | ||
| } | ||
| clearStoredFirstRozaDate(); | ||
| return null; | ||
| }; | ||
| var getTargetRamadanYear = (today) => { | ||
| const hijriYear = Number.parseInt(today.date.hijri.year, 10); | ||
| const hijriMonth = today.date.hijri.month.number; | ||
| if (hijriMonth > 9) { | ||
| return hijriYear + 1; | ||
| } | ||
| return hijriYear; | ||
| }; | ||
| var formatRowAnnotation = (kind) => { | ||
| if (kind === "current") { | ||
| return pc4.green("\u2190 current"); | ||
| } | ||
| return pc4.yellow("\u2190 next"); | ||
| }; | ||
| var printTable = (rows, rowAnnotations = {}) => { | ||
| const headers = ["Roza", "Sehar", "Iftar", "Date", "Hijri"]; | ||
| const widths = [6, 8, 8, 14, 20]; | ||
| const pad = (value, index) => value.padEnd(widths[index] ?? value.length); | ||
| const line = (columns) => columns.map((column, index) => pad(column, index)).join(" "); | ||
| const divider = "-".repeat(line(headers).length); | ||
| console.log(pc4.dim(` ${line(headers)}`)); | ||
| console.log(pc4.dim(` ${divider}`)); | ||
| for (const row of rows) { | ||
| const rowLine = line([ | ||
| String(row.roza), | ||
| row.sehar, | ||
| row.iftar, | ||
| row.date, | ||
| row.hijri | ||
| ]); | ||
| const annotation = rowAnnotations[row.roza]; | ||
| if (!annotation) { | ||
| console.log(` ${rowLine}`); | ||
| continue; | ||
| } | ||
| console.log(` ${rowLine} ${formatRowAnnotation(annotation)}`); | ||
| } | ||
| }; | ||
| var getErrorMessage = (error) => { | ||
| if (error instanceof Error) { | ||
| return error.message; | ||
| } | ||
| return "unknown error"; | ||
| }; | ||
| var getJsonErrorCode = (message) => { | ||
| if (message.startsWith("Invalid first roza date")) { | ||
| return "INVALID_FIRST_ROZA_DATE"; | ||
| } | ||
| if (message.includes("Use either --all or --number")) { | ||
| return "INVALID_FLAG_COMBINATION"; | ||
| } | ||
| if (message.startsWith("Could not fetch prayer times.")) { | ||
| return "PRAYER_TIMES_FETCH_FAILED"; | ||
| } | ||
| if (message.startsWith("Could not fetch Ramadan calendar.")) { | ||
| return "RAMADAN_CALENDAR_FETCH_FAILED"; | ||
| } | ||
| if (message.startsWith("Could not detect location.")) { | ||
| return "LOCATION_DETECTION_FAILED"; | ||
| } | ||
| if (message.startsWith("Could not find roza")) { | ||
| return "ROZA_NOT_FOUND"; | ||
| } | ||
| if (message === "unknown error") { | ||
| return "UNKNOWN_ERROR"; | ||
| } | ||
| return "RAMADAN_CLI_ERROR"; | ||
| }; | ||
| var toJsonErrorPayload = (error) => { | ||
| const message = getErrorMessage(error); | ||
| return { | ||
| ok: false, | ||
| error: { | ||
| code: getJsonErrorCode(message), | ||
| message | ||
| } | ||
| }; | ||
| }; | ||
| var parseCityCountry = (value) => { | ||
| const parts = value.split(",").map((part) => part.trim()).filter(Boolean); | ||
| if (parts.length < 2) { | ||
| return null; | ||
| } | ||
| const city = normalizeCityAlias(parts[0] ?? ""); | ||
| if (!city) { | ||
| return null; | ||
| } | ||
| const country = parts.slice(1).join(", ").trim(); | ||
| if (!country) { | ||
| return null; | ||
| } | ||
| return { city, country }; | ||
| }; | ||
| var getAddressFromGuess = (guessed) => `${guessed.city}, ${guessed.country}`; | ||
| var withStoredSettings = (query) => { | ||
| const settings = getStoredPrayerSettings(); | ||
| const withMethodSchool = { | ||
| ...query, | ||
| method: settings.method, | ||
| school: settings.school | ||
| }; | ||
| if (!settings.timezone) { | ||
| return withMethodSchool; | ||
| } | ||
| return { | ||
| ...withMethodSchool, | ||
| timezone: settings.timezone | ||
| }; | ||
| }; | ||
| var withCountryAwareSettings = (query, country, cityTimezone) => { | ||
| const settings = getStoredPrayerSettings(); | ||
| let method = settings.method; | ||
| const recommendedMethod = getRecommendedMethod(country); | ||
| if (recommendedMethod !== null && shouldApplyRecommendedMethod(settings.method, recommendedMethod)) { | ||
| method = recommendedMethod; | ||
| } | ||
| let school = settings.school; | ||
| const recommendedSchool = getRecommendedSchool(country); | ||
| if (shouldApplyRecommendedSchool(settings.school, recommendedSchool)) { | ||
| school = recommendedSchool; | ||
| } | ||
| const timezone = cityTimezone ?? settings.timezone; | ||
| return { | ||
| ...query, | ||
| method, | ||
| school, | ||
| ...timezone ? { timezone } : {} | ||
| }; | ||
| }; | ||
| var getStoredQuery = () => { | ||
| if (!hasStoredLocation()) { | ||
| return null; | ||
| } | ||
| const location = getStoredLocation(); | ||
| if (location.city && location.country) { | ||
| const cityCountryQuery = { | ||
| address: `${location.city}, ${location.country}`, | ||
| city: location.city, | ||
| country: location.country, | ||
| ...location.latitude !== void 0 ? { latitude: location.latitude } : {}, | ||
| ...location.longitude !== void 0 ? { longitude: location.longitude } : {} | ||
| }; | ||
| return withStoredSettings({ | ||
| ...cityCountryQuery | ||
| }); | ||
| } | ||
| if (location.latitude !== void 0 && location.longitude !== void 0) { | ||
| return withStoredSettings({ | ||
| address: `${location.latitude}, ${location.longitude}`, | ||
| latitude: location.latitude, | ||
| longitude: location.longitude | ||
| }); | ||
| } | ||
| return null; | ||
| }; | ||
| var resolveQueryFromCityInput = async (city) => { | ||
| const normalizedInput = normalizeCityAlias(city); | ||
| const parsed = parseCityCountry(normalizedInput); | ||
| if (parsed) { | ||
| return withCountryAwareSettings( | ||
| { | ||
| address: `${parsed.city}, ${parsed.country}`, | ||
| city: parsed.city, | ||
| country: parsed.country | ||
| }, | ||
| parsed.country | ||
| ); | ||
| } | ||
| const guessed = await guessCityCountry(normalizedInput); | ||
| if (!guessed) { | ||
| return withStoredSettings({ address: normalizedInput }); | ||
| } | ||
| return withCountryAwareSettings( | ||
| { | ||
| address: `${guessed.city}, ${guessed.country}`, | ||
| city: guessed.city, | ||
| country: guessed.country, | ||
| latitude: guessed.latitude, | ||
| longitude: guessed.longitude | ||
| }, | ||
| guessed.country, | ||
| guessed.timezone | ||
| ); | ||
| }; | ||
| var resolveQuery = async (opts) => { | ||
| const city = opts.city; | ||
| if (city) { | ||
| return await resolveQueryFromCityInput(city); | ||
| } | ||
| const storedQuery = getStoredQuery(); | ||
| if (storedQuery) { | ||
| return storedQuery; | ||
| } | ||
| if (opts.allowInteractiveSetup && canPromptInteractively()) { | ||
| const configured = await runFirstRunSetup(); | ||
| if (!configured) { | ||
| process.exit(0); | ||
| } | ||
| const configuredQuery = getStoredQuery(); | ||
| if (configuredQuery) { | ||
| return configuredQuery; | ||
| } | ||
| } | ||
| const guessed = await guessLocation(); | ||
| if (!guessed) { | ||
| throw new Error( | ||
| 'Could not detect location. Pass a city like `ramadan-cli "Lahore"`.' | ||
| ); | ||
| } | ||
| saveAutoDetectedSetup(guessed); | ||
| return withStoredSettings({ | ||
| address: getAddressFromGuess(guessed), | ||
| city: guessed.city, | ||
| country: guessed.country, | ||
| latitude: guessed.latitude, | ||
| longitude: guessed.longitude | ||
| }); | ||
| }; | ||
| var fetchRamadanDay = async (query, date) => { | ||
| const errors = []; | ||
| const addressOptions = { | ||
| address: query.address | ||
| }; | ||
| if (query.method !== void 0) { | ||
| addressOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| addressOptions.school = query.school; | ||
| } | ||
| if (date) { | ||
| addressOptions.date = date; | ||
| } | ||
| try { | ||
| return await fetchTimingsByAddress(addressOptions); | ||
| } catch (error) { | ||
| errors.push(`timingsByAddress failed: ${getErrorMessage(error)}`); | ||
| } | ||
| if (query.city && query.country) { | ||
| const cityOptions = { | ||
| city: query.city, | ||
| country: query.country | ||
| }; | ||
| if (query.method !== void 0) { | ||
| cityOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| cityOptions.school = query.school; | ||
| } | ||
| if (date) { | ||
| cityOptions.date = date; | ||
| } | ||
| try { | ||
| return await fetchTimingsByCity(cityOptions); | ||
| } catch (error) { | ||
| errors.push(`timingsByCity failed: ${getErrorMessage(error)}`); | ||
| } | ||
| } | ||
| if (query.latitude !== void 0 && query.longitude !== void 0) { | ||
| const coordsOptions = { | ||
| latitude: query.latitude, | ||
| longitude: query.longitude | ||
| }; | ||
| if (query.method !== void 0) { | ||
| coordsOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| coordsOptions.school = query.school; | ||
| } | ||
| if (query.timezone) { | ||
| coordsOptions.timezone = query.timezone; | ||
| } | ||
| if (date) { | ||
| coordsOptions.date = date; | ||
| } | ||
| try { | ||
| return await fetchTimingsByCoords(coordsOptions); | ||
| } catch (error) { | ||
| errors.push(`timingsByCoords failed: ${getErrorMessage(error)}`); | ||
| } | ||
| } | ||
| throw new Error(`Could not fetch prayer times. ${errors.join(" | ")}`); | ||
| }; | ||
| var fetchRamadanCalendar = async (query, year) => { | ||
| const errors = []; | ||
| const addressOptions = { | ||
| address: query.address, | ||
| year, | ||
| month: 9 | ||
| }; | ||
| if (query.method !== void 0) { | ||
| addressOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| addressOptions.school = query.school; | ||
| } | ||
| try { | ||
| return await fetchHijriCalendarByAddress(addressOptions); | ||
| } catch (error) { | ||
| errors.push(`hijriCalendarByAddress failed: ${getErrorMessage(error)}`); | ||
| } | ||
| if (query.city && query.country) { | ||
| const cityOptions = { | ||
| city: query.city, | ||
| country: query.country, | ||
| year, | ||
| month: 9 | ||
| }; | ||
| if (query.method !== void 0) { | ||
| cityOptions.method = query.method; | ||
| } | ||
| if (query.school !== void 0) { | ||
| cityOptions.school = query.school; | ||
| } | ||
| try { | ||
| return await fetchHijriCalendarByCity(cityOptions); | ||
| } catch (error) { | ||
| errors.push(`hijriCalendarByCity failed: ${getErrorMessage(error)}`); | ||
| } | ||
| } | ||
| throw new Error(`Could not fetch Ramadan calendar. ${errors.join(" | ")}`); | ||
| }; | ||
| var fetchCustomRamadanDays = async (query, firstRozaDate) => { | ||
| const totalDays = 30; | ||
| const days = Array.from( | ||
| { length: totalDays }, | ||
| (_, index) => addDays(firstRozaDate, index) | ||
| ); | ||
| return Promise.all( | ||
| days.map(async (dayDate) => fetchRamadanDay(query, dayDate)) | ||
| ); | ||
| }; | ||
| var getRowByRozaNumber = (days, rozaNumber) => { | ||
| const day = days[rozaNumber - 1]; | ||
| if (!day) { | ||
| throw new Error(`Could not find roza ${rozaNumber} timings.`); | ||
| } | ||
| return toRamadanRow(day, rozaNumber); | ||
| }; | ||
| var getDayByRozaNumber = (days, rozaNumber) => { | ||
| const day = days[rozaNumber - 1]; | ||
| if (!day) { | ||
| throw new Error(`Could not find roza ${rozaNumber} timings.`); | ||
| } | ||
| return day; | ||
| }; | ||
| var getHijriYearFromRozaNumber = (days, rozaNumber, fallbackYear) => { | ||
| const day = days[rozaNumber - 1]; | ||
| if (!day) { | ||
| return fallbackYear; | ||
| } | ||
| return Number.parseInt(day.date.hijri.year, 10); | ||
| }; | ||
| var setRowAnnotation = (annotations, roza, kind) => { | ||
| if (roza < 1 || roza > 30) { | ||
| return; | ||
| } | ||
| annotations[roza] = kind; | ||
| }; | ||
| var getAllModeRowAnnotations = (input) => { | ||
| const annotations = {}; | ||
| if (input.configuredFirstRozaDate) { | ||
| const currentRoza2 = getRozaNumberFromStartDate( | ||
| input.configuredFirstRozaDate, | ||
| input.todayGregorianDate | ||
| ); | ||
| if (currentRoza2 < 1) { | ||
| setRowAnnotation(annotations, 1, "next"); | ||
| return annotations; | ||
| } | ||
| setRowAnnotation(annotations, currentRoza2, "current"); | ||
| setRowAnnotation(annotations, currentRoza2 + 1, "next"); | ||
| return annotations; | ||
| } | ||
| const todayHijriYear = Number.parseInt(input.today.date.hijri.year, 10); | ||
| const isRamadanNow = input.today.date.hijri.month.number === 9 && todayHijriYear === input.targetYear; | ||
| if (!isRamadanNow) { | ||
| setRowAnnotation(annotations, 1, "next"); | ||
| return annotations; | ||
| } | ||
| const currentRoza = getRozaNumberFromHijriDay(input.today); | ||
| setRowAnnotation(annotations, currentRoza, "current"); | ||
| setRowAnnotation(annotations, currentRoza + 1, "next"); | ||
| return annotations; | ||
| }; | ||
| var printTextOutput = (output, plain, highlight, rowAnnotations = {}) => { | ||
| const title = output.mode === "all" ? `Ramadan ${output.hijriYear} (All Days)` : output.mode === "number" ? `Roza ${output.rows[0]?.roza ?? ""} Sehar/Iftar` : "Today Sehar/Iftar"; | ||
| console.log(plain ? "RAMADAN CLI" : getBanner()); | ||
| console.log(ramadanGreen(` ${title}`)); | ||
| console.log(pc4.dim(` \u{1F4CD} ${output.location}`)); | ||
| console.log(""); | ||
| printTable(output.rows, rowAnnotations); | ||
| console.log(""); | ||
| if (highlight) { | ||
| console.log(` ${ramadanGreen("Status:")} ${pc4.white(highlight.current)}`); | ||
| console.log( | ||
| ` ${ramadanGreen("Up next:")} ${pc4.white(highlight.next)} in ${pc4.yellow(highlight.countdown)}` | ||
| ); | ||
| console.log(""); | ||
| } | ||
| console.log(pc4.dim(" Sehar uses Fajr. Iftar uses Maghrib.")); | ||
| console.log(""); | ||
| }; | ||
| var ramadanCommand = async (opts) => { | ||
| const spinner2 = opts.json ? null : ora({ | ||
| text: "Fetching Ramadan timings...", | ||
| stream: process.stdout | ||
| }); | ||
| try { | ||
| const configuredFirstRozaDate = getConfiguredFirstRozaDate(opts); | ||
| const query = await resolveQuery({ | ||
| city: opts.city, | ||
| allowInteractiveSetup: !opts.json | ||
| }); | ||
| spinner2?.start(); | ||
| const today = await fetchRamadanDay(query); | ||
| const todayGregorianDate = parseGregorianDate(today.date.gregorian.date); | ||
| if (!todayGregorianDate) { | ||
| throw new Error("Could not parse Gregorian date from prayer response."); | ||
| } | ||
| const targetYear = getTargetRamadanYear(today); | ||
| const hasCustomFirstRozaDate = configuredFirstRozaDate !== null; | ||
| if (opts.all && opts.rozaNumber !== void 0) { | ||
| throw new Error("Use either --all or --number, not both."); | ||
| } | ||
| if (opts.rozaNumber !== void 0) { | ||
| let row; | ||
| let hijriYear2 = targetYear; | ||
| let selectedDay; | ||
| if (hasCustomFirstRozaDate) { | ||
| const firstRozaDate = configuredFirstRozaDate; | ||
| if (!firstRozaDate) { | ||
| throw new Error("Could not determine first roza date."); | ||
| } | ||
| const customDays = await fetchCustomRamadanDays(query, firstRozaDate); | ||
| row = getRowByRozaNumber(customDays, opts.rozaNumber); | ||
| selectedDay = getDayByRozaNumber(customDays, opts.rozaNumber); | ||
| hijriYear2 = getHijriYearFromRozaNumber( | ||
| customDays, | ||
| opts.rozaNumber, | ||
| targetYear | ||
| ); | ||
| } else { | ||
| const calendar = await fetchRamadanCalendar(query, targetYear); | ||
| row = getRowByRozaNumber(calendar, opts.rozaNumber); | ||
| selectedDay = getDayByRozaNumber(calendar, opts.rozaNumber); | ||
| hijriYear2 = getHijriYearFromRozaNumber( | ||
| calendar, | ||
| opts.rozaNumber, | ||
| targetYear | ||
| ); | ||
| } | ||
| const output2 = { | ||
| mode: "number", | ||
| location: query.address, | ||
| hijriYear: hijriYear2, | ||
| rows: [row] | ||
| }; | ||
| spinner2?.stop(); | ||
| if (opts.json) { | ||
| console.log(JSON.stringify(output2, null, 2)); | ||
| return; | ||
| } | ||
| printTextOutput( | ||
| output2, | ||
| Boolean(opts.plain), | ||
| getHighlightState(selectedDay) | ||
| ); | ||
| return; | ||
| } | ||
| if (!opts.all) { | ||
| let row = null; | ||
| let outputHijriYear = targetYear; | ||
| let highlightDay = null; | ||
| if (hasCustomFirstRozaDate) { | ||
| const firstRozaDate = configuredFirstRozaDate; | ||
| if (!firstRozaDate) { | ||
| throw new Error("Could not determine first roza date."); | ||
| } | ||
| const rozaNumber = getRozaNumberFromStartDate( | ||
| firstRozaDate, | ||
| todayGregorianDate | ||
| ); | ||
| if (rozaNumber < 1) { | ||
| const firstRozaDay = await fetchRamadanDay(query, firstRozaDate); | ||
| row = toRamadanRow(firstRozaDay, 1); | ||
| highlightDay = firstRozaDay; | ||
| outputHijriYear = Number.parseInt(firstRozaDay.date.hijri.year, 10); | ||
| } | ||
| if (rozaNumber >= 1) { | ||
| row = toRamadanRow(today, rozaNumber); | ||
| highlightDay = today; | ||
| outputHijriYear = Number.parseInt(today.date.hijri.year, 10); | ||
| } | ||
| } | ||
| if (!hasCustomFirstRozaDate) { | ||
| const isRamadanNow = today.date.hijri.month.number === 9; | ||
| if (isRamadanNow) { | ||
| row = toRamadanRow(today, getRozaNumberFromHijriDay(today)); | ||
| highlightDay = today; | ||
| } | ||
| if (!isRamadanNow) { | ||
| const calendar = await fetchRamadanCalendar(query, targetYear); | ||
| const firstRamadanDay = calendar[0]; | ||
| if (!firstRamadanDay) { | ||
| throw new Error("Could not find the first day of Ramadan."); | ||
| } | ||
| row = toRamadanRow(firstRamadanDay, 1); | ||
| highlightDay = firstRamadanDay; | ||
| outputHijriYear = Number.parseInt( | ||
| firstRamadanDay.date.hijri.year, | ||
| 10 | ||
| ); | ||
| } | ||
| } | ||
| if (!row) { | ||
| throw new Error("Could not determine roza number."); | ||
| } | ||
| const output2 = { | ||
| mode: "today", | ||
| location: query.address, | ||
| hijriYear: outputHijriYear, | ||
| rows: [row] | ||
| }; | ||
| spinner2?.stop(); | ||
| if (opts.json) { | ||
| console.log(JSON.stringify(output2, null, 2)); | ||
| return; | ||
| } | ||
| printTextOutput( | ||
| output2, | ||
| Boolean(opts.plain), | ||
| getHighlightState(highlightDay ?? today) | ||
| ); | ||
| return; | ||
| } | ||
| let rows = []; | ||
| let hijriYear = targetYear; | ||
| if (hasCustomFirstRozaDate) { | ||
| const firstRozaDate = configuredFirstRozaDate; | ||
| if (!firstRozaDate) { | ||
| throw new Error("Could not determine first roza date."); | ||
| } | ||
| const customDays = await fetchCustomRamadanDays(query, firstRozaDate); | ||
| rows = customDays.map((day, index) => toRamadanRow(day, index + 1)); | ||
| const firstCustomDay = customDays[0]; | ||
| if (firstCustomDay) { | ||
| hijriYear = Number.parseInt(firstCustomDay.date.hijri.year, 10); | ||
| } | ||
| } | ||
| if (!hasCustomFirstRozaDate) { | ||
| const calendar = await fetchRamadanCalendar(query, targetYear); | ||
| rows = calendar.map((day, index) => toRamadanRow(day, index + 1)); | ||
| } | ||
| const output = { | ||
| mode: "all", | ||
| location: query.address, | ||
| hijriYear, | ||
| rows | ||
| }; | ||
| const allModeRowAnnotations = getAllModeRowAnnotations({ | ||
| today, | ||
| todayGregorianDate, | ||
| targetYear, | ||
| configuredFirstRozaDate | ||
| }); | ||
| spinner2?.stop(); | ||
| if (opts.json) { | ||
| console.log(JSON.stringify(output, null, 2)); | ||
| return; | ||
| } | ||
| printTextOutput( | ||
| output, | ||
| Boolean(opts.plain), | ||
| getHighlightState(today), | ||
| allModeRowAnnotations | ||
| ); | ||
| } catch (error) { | ||
| if (opts.json) { | ||
| process.stderr.write(`${JSON.stringify(toJsonErrorPayload(error))} | ||
| `); | ||
| process.exit(1); | ||
| } | ||
| spinner2?.fail( | ||
| error instanceof Error ? error.message : "Failed to fetch Ramadan timings" | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| }; | ||
| export { | ||
| getRecommendedMethod, | ||
| getRecommendedSchool, | ||
| shouldApplyRecommendedMethod, | ||
| shouldApplyRecommendedSchool, | ||
| getStoredLocation, | ||
| hasStoredLocation, | ||
| getStoredPrayerSettings, | ||
| setStoredLocation, | ||
| setStoredTimezone, | ||
| setStoredMethod, | ||
| setStoredSchool, | ||
| getStoredFirstRozaDate, | ||
| setStoredFirstRozaDate, | ||
| clearStoredFirstRozaDate, | ||
| clearRamadanConfig, | ||
| saveAutoDetectedSetup, | ||
| applyRecommendedSettingsIfUnset, | ||
| configCommand, | ||
| fetchTimingsByCity, | ||
| fetchTimingsByAddress, | ||
| fetchTimingsByCoords, | ||
| fetchNextPrayer, | ||
| fetchCalendarByCity, | ||
| fetchCalendarByAddress, | ||
| fetchHijriCalendarByAddress, | ||
| fetchHijriCalendarByCity, | ||
| fetchMethods, | ||
| fetchQibla, | ||
| guessLocation, | ||
| guessCityCountry, | ||
| ramadanCommand | ||
| }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 5 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 5 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
82611
1.49%2372
1.37%207
3.5%8
14.29%