weather-cli-16bit
Version:
A simple, intelligent weather CLI with smart location detection - no quotes needed
239 lines (200 loc) • 7.67 kB
JavaScript
import chalk from 'chalk';
import ora from 'ora';
import httpClient from './api/http.js';
import { getApiKey } from './api/auth.js';
import { WeatherError, ERROR_CODES } from './utils/errors.js';
import { validateLocation, validateCoordinates } from './utils/validators.js';
const BASE_URL = 'https://api.openweathermap.org/data/2.5';
// Regional temperature units mapping
const FAHRENHEIT_COUNTRIES = new Set([
'US', 'USA', 'BS', 'BZ', 'KY', 'PW' // US, Bahamas, Belize, Cayman Islands, Palau
]);
// Temperature conversion utilities
function celsiusToFahrenheit(celsius) {
return (celsius * 9/5) + 32;
}
function fahrenheitToCelsius(fahrenheit) {
return (fahrenheit - 32) * 5/9;
}
// Detect regional temperature unit preference
function getRegionalTempUnit(countryCode) {
return FAHRENHEIT_COUNTRIES.has(countryCode?.toUpperCase()) ? 'fahrenheit' : 'celsius';
}
// Convert temperature between units
function convertTemperature(temp, fromUnit, toUnit) {
if (fromUnit === toUnit) return temp;
if (fromUnit === 'celsius' && toUnit === 'fahrenheit') {
return celsiusToFahrenheit(temp);
} else if (fromUnit === 'fahrenheit' && toUnit === 'celsius') {
return fahrenheitToCelsius(temp);
}
return temp;
}
// Determine display unit system
function determineDisplayUnits(countryCode, userPreference = null) {
// Manual override takes precedence
if (userPreference === 'fahrenheit' || userPreference === 'imperial') {
return { api: 'imperial', display: 'fahrenheit' };
}
if (userPreference === 'celsius' || userPreference === 'metric') {
return { api: 'metric', display: 'celsius' };
}
// Auto-detect based on country
const regionalUnit = getRegionalTempUnit(countryCode);
return {
api: regionalUnit === 'fahrenheit' ? 'imperial' : 'metric',
display: regionalUnit
};
}
// Get weather by coordinates
async function getWeatherByCoords(lat, lon, userUnits = null) {
const { latitude, longitude } = validateCoordinates(lat, lon);
const apiKey = await getApiKey();
const spinner = ora('Fetching weather data...').start();
try {
// Get current weather (metric first to determine country)
const weatherResponse = await httpClient.get(`${BASE_URL}/weather`, {
params: {
lat: latitude,
lon: longitude,
appid: apiKey,
units: 'metric'
}
});
const countryCode = weatherResponse.data.sys.country;
const unitSystem = determineDisplayUnits(countryCode, userUnits);
// Get final weather data in correct units
let finalWeatherData = weatherResponse.data;
if (unitSystem.api !== 'metric') {
const weatherResponseFinal = await httpClient.get(`${BASE_URL}/weather`, {
params: {
lat: latitude,
lon: longitude,
appid: apiKey,
units: unitSystem.api
}
});
finalWeatherData = weatherResponseFinal.data;
}
// Get 5-day forecast
const forecastResponse = await httpClient.get(`${BASE_URL}/forecast`, {
params: {
lat: latitude,
lon: longitude,
appid: apiKey,
units: unitSystem.api
}
});
// Get air pollution data for alerts
const pollutionResponse = await httpClient.get(`${BASE_URL}/air_pollution`, {
params: {
lat: latitude,
lon: longitude,
appid: apiKey
}
});
spinner.succeed(`Weather data fetched! Using ${unitSystem.display === 'fahrenheit' ? 'Fahrenheit' : 'Celsius'} for ${countryCode}`);
return {
current: finalWeatherData,
forecast: forecastResponse.data,
pollution: pollutionResponse.data,
displayUnit: unitSystem.display,
countryCode: countryCode
};
} catch (error) {
spinner.fail('Failed to fetch weather data');
if (error.response?.status === 404) {
throw new WeatherError('Location not found', ERROR_CODES.LOCATION_NOT_FOUND, 404);
} else if (error.response?.status === 401) {
throw new WeatherError('Invalid API key', ERROR_CODES.API_KEY_INVALID, 401);
} else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
throw new WeatherError('Request timed out. Please try again.', ERROR_CODES.NETWORK_ERROR);
} else if (error.response?.status === 429) {
throw new WeatherError(error.message, ERROR_CODES.RATE_LIMIT, 429);
}
throw new WeatherError(
`Network error: ${error.message}`,
ERROR_CODES.NETWORK_ERROR
);
}
}
// Fetch weather data by location
async function getWeather(location, userUnits = null) {
const validatedLocation = validateLocation(location);
const apiKey = await getApiKey();
const spinner = ora('Fetching weather data...').start();
try {
// First, get location info to determine regional preferences
const weatherResponse = await httpClient.get(`${BASE_URL}/weather`, {
params: {
q: validatedLocation,
appid: apiKey,
units: 'metric' // Always fetch in metric first to get country code
}
});
const countryCode = weatherResponse.data.sys.country;
const unitSystem = determineDisplayUnits(countryCode, userUnits);
// If we need different units than metric, fetch again
let finalWeatherData = weatherResponse.data;
let forecastData, pollutionData;
if (unitSystem.api !== 'metric') {
const weatherResponseFinal = await httpClient.get(`${BASE_URL}/weather`, {
params: {
q: validatedLocation,
appid: apiKey,
units: unitSystem.api
}
});
finalWeatherData = weatherResponseFinal.data;
}
// Get 5-day forecast
const forecastResponse = await httpClient.get(`${BASE_URL}/forecast`, {
params: {
q: validatedLocation,
appid: apiKey,
units: unitSystem.api
}
});
// Get air pollution data for alerts
const pollutionResponse = await httpClient.get(`${BASE_URL}/air_pollution`, {
params: {
lat: finalWeatherData.coord.lat,
lon: finalWeatherData.coord.lon,
appid: apiKey
}
});
spinner.succeed(`Weather data fetched! Using ${unitSystem.display === 'fahrenheit' ? 'Fahrenheit' : 'Celsius'} for ${countryCode}`);
const data = {
current: finalWeatherData,
forecast: forecastResponse.data,
pollution: pollutionResponse.data,
displayUnit: unitSystem.display,
countryCode: countryCode
};
return data;
} catch (error) {
spinner.fail('Failed to fetch weather data');
if (error.response?.status === 404) {
throw new WeatherError(
`Location "${location}" not found. Please check the spelling or try: "City, Country Code" (e.g., "San Ramon, US")`,
ERROR_CODES.LOCATION_NOT_FOUND,
404
);
} else if (error.response?.status === 401) {
throw new WeatherError('Invalid API key', ERROR_CODES.API_KEY_INVALID, 401);
} else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
throw new WeatherError('Request timed out. Please try again.', ERROR_CODES.NETWORK_ERROR);
} else if (error.response?.status === 429) {
throw new WeatherError(error.message, ERROR_CODES.RATE_LIMIT, 429);
}
throw error;
}
}
export {
getWeather,
getWeatherByCoords,
determineDisplayUnits,
convertTemperature,
celsiusToFahrenheit,
fahrenheitToCelsius
};