Socket
Book a DemoSign in
Socket

ramadan-cli

Package Overview
Dependencies
Maintainers
1
Versions
31
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ramadan-cli - npm Package Compare versions

Comparing version
6.0.1
to
6.1.0
+2031
dist/chunk-MFEFTDJ3.js
#!/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;

@@ -34,3 +34,3 @@ #!/usr/bin/env node

shouldApplyRecommendedSchool
} from "./chunk-ZE6KRFJM.js";
} from "./chunk-MFEFTDJ3.js";
export {

@@ -37,0 +37,0 @@ applyRecommendedSettingsIfUnset,

{
"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",

@@ -23,2 +23,3 @@ [![ramadan-cli](https://raw.githubusercontent.com/ahmadawais/ramadan-cli/refs/heads/main/.github/cover.png)](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
};