A lightweight, interactive command-line application built with TypeScript that lets you look up real-time weather conditions for any city in the world — right from your terminal. No API key required.
- Purpose
- Tech Stack
- Project Structure
- Getting Started
- CLI Options
- Available Scripts
- Example Sessions
- Error Handling
- Architecture & File Interaction Flow
- Running Tests
Weather CLI is a terminal-based tool that accepts a city name as input and returns:
- 📍 Location — resolved city name and country
- 🌡️ Temperature — current temperature in °C or °F
- 💨 Wind Speed — current wind speed in km/h or mph
The app runs in a continuous loop, letting you query multiple cities in a single session. It remembers your last city as a default, supports metric and imperial units, handles invalid cities gracefully, and survives network failures without crashing.
It can also auto-detect your current location via IP geolocation — either at startup (when no default city is saved) or on demand with the -c flag — and fetch the weather immediately without typing a city name.
| Package | Role |
|---|---|
typescript |
Strongly-typed language that compiles to JS |
commander |
CLI argument & option parsing |
axios |
HTTP client for calling weather APIs |
inquirer |
Interactive terminal prompts |
chalk |
Coloured, styled terminal output |
ora |
Spinner animation during API calls |
zod |
Runtime schema validation for API responses |
conf |
Persistent config storage (default city, unit) |
jest |
Test runner |
ts-jest |
TypeScript transformer for Jest |
tsx |
TypeScript runner for development (no build step) |
External APIs used (free, no API key needed):
- Open-Meteo Geocoding API — converts a city name to latitude/longitude
- Open-Meteo Weather API — returns current weather for a coordinate pair
- ip-api.com — resolves the caller's public IP to a city name and coordinates (free tier, HTTP only)
WeatherCLI/
├── src/
│ ├── index.ts # Entry point — shebang + calls runCLI()
│ ├── cli.ts # Commander setup — flags, app name, version, dispatch
│ ├── cli.test.ts # Unit tests for runCLI() — all flags, dispatch, aliases
│ ├── app.ts # Interactive weather loop (startApp) + auto-detect on first run
│ ├── index.test.ts # Integration tests for startApp() incl. auto-detect flow
│ ├── services/
│ │ ├── weather.ts # getWeatherData (city→coords→weather) + getWeatherByCoords
│ │ ├── weather.test.ts # Unit tests for getWeatherData() and getWeatherByCoords()
│ │ ├── location.ts # IP geolocation service — getLocationByIP
│ │ └── location.test.ts # Unit tests for getLocationByIP()
│ ├── types/
│ │ ├── Response.ts # Zod schemas + inferred types for API responses
│ │ └── Config.ts # TypeScript interface for persistent config schema
│ ├── ui/
│ │ ├── display.ts # Console output formatting
│ │ └── display.test.ts # Unit tests for display functions
│ └── utils/
│ └── config.ts # Conf instance — persistent config (default city, unit)
├── dist/ # Compiled JavaScript output (auto-generated)
├── jest.config.cjs # Jest configuration
├── tsconfig.json # TypeScript compiler options
├── tsconfig.jest.json # TypeScript overrides for ts-jest (NodeNext module mode)
└── package.json
- Node.js v18 or higher
- npm v9 or higher
git clone https://github.com/your-username/WeatherCLI.git
cd WeatherCLI
npm install
npm run build
This compiles the TypeScript source files into dist/.
npm link
This creates a global symlink from your npm bin directory to dist/index.js, powered by the bin entry in package.json. After this step you can use weather as a terminal command from any directory.
weather
To unlink later: run
npm unlinkfrom the project directory.
npm start # build + run once
npm run dev # run directly via tsx (no build step, dev mode)
Usage: weather [options]
Simple Weather CLI with TypeScript
Options:
-v, --version Output the current version
-c, --current Get weather for your current location (via IP) and exit
-u, --unit <unit> Set the unit system and exit
-s, --show-settings Show current default city and unit settings and exit
--clear-default Clear the saved default city and exit
-h, --help Display help for command
| Option | Accepted values | Description |
|---|---|---|
-c, --current |
— | Detect location via IP and print current weather, then exit |
-u, --unit <unit> |
metric, imperial, m, i |
Set the unit system — persisted across sessions |
-s, --show-settings |
— | Print the current default city and unit, then exit |
--clear-default |
— | Remove the saved default city from config |
-v, --version |
— | Print the current version number |
-h, --help |
— | Display the help message |
Shorthand for
--unit:mis an alias formetric,iis an alias forimperial. All values are case-insensitive (M,Imperial, etc. all work).
| Command | Description |
|---|---|
npm run build |
Compile TypeScript to dist/ |
npm start |
Compile TypeScript then run the app |
npm run dev |
Run the app directly via tsx (no build needed) |
npm test |
Run the full test suite once |
npm run test:watch |
Run tests in interactive watch mode |
npm run test:coverage |
Run tests and generate a coverage report |
When no default city is saved, the app offers to detect your location automatically before entering the prompt loop.
$ weather
☀️ Welcome to Weather CLI
? No saved city found — detect weather for your current location? Yes
⠋ Detecting your location...
✔ Location detected!
📍 Location: Bangkok, Thailand
🌡️ Temperature: 33°C
💨 Wind Speed: 14 km/h
? Save "Bangkok" as your default city? Yes
(💾 Saved "Bangkok" as default city. Run 'weather --clear-default' to clear it)
? Type city name (or 'q' to quit): q
👋 Goodbye!
$ weather
☀️ Welcome to Weather CLI
? No saved city found — detect weather for your current location? No
? Type city name (or 'q' to quit): Bangkok
? Save this city as default for next time? Yes
(💾 Saved "Bangkok" as default city. Run 'weather --clear-default' to clear it)
⠋ Now checking the weather in Bangkok...
✔ Weather data retrieved successfully!
📍 Location: Bangkok, Thailand
🌡️ Temperature: 33°C
💨 Wind Speed: 14 km/h
? Type city name (or 'q' to quit): q
👋 Goodbye!
$ weather
☀️ Welcome to Weather CLI
? Type city name (or 'q' to quit): (Bangkok) ← pre-filled, press Enter to use
✔ Weather data retrieved successfully!
📍 Location: Bangkok, Thailand
🌡️ Temperature: 31°C
💨 Wind Speed: 9 km/h
? Type city name (or 'q' to quit): q
👋 Goodbye!
$ weather --unit i
✅ Unit set to imperial (°F / mph).
$ weather --unit imperial
✅ Unit set to imperial (°F / mph).
$ weather --unit m
✅ Unit set to metric (°C / km/h).
$ weather --unit banana
❌ Invalid unit "banana". Choose "metric", "imperial", "m", or "i".
Once set, the unit is remembered for all future sessions until changed.
$ weather
☀️ Welcome to Weather CLI
? Type city name (or 'q' to quit): Bangkok
✔ Weather data retrieved successfully!
📍 Location: Bangkok, Thailand
🌡️ Temperature: 91.4°F
💨 Wind Speed: 8.7 mph
? Type city name (or 'q' to quit): q
👋 Goodbye!
$ weather -c
✔ Weather data retrieved!
📍 Location: Bangkok, Thailand
🌡️ Temperature: 33°C
💨 Wind Speed: 14 km/h
Uses the unit saved in config (or metric by default). Exits immediately after printing — no prompt loop.
$ weather --show-settings
📋 Current Settings:
Default City: Bangkok
Unit: metric
$ weather -s
📋 Current Settings:
Default City: None
Unit: Not set (defaults to metric)
$ weather --clear-default
✅ Default city "Bangkok" has been cleared.
$ weather --clear-default
⚠️ No default city was set.
$ weather
☀️ Welcome to Weather CLI
? Type city name (or 'q' to quit): Tokyo
✔ Weather data retrieved successfully!
📍 Location: Tokyo, Japan
🌡️ Temperature: 18°C
💨 Wind Speed: 20 km/h
? Type city name (or 'q' to quit): Helsinki
✔ Weather data retrieved successfully!
📍 Location: Helsinki, Finland
🌡️ Temperature: 3°C
💨 Wind Speed: 28 km/h
? Type city name (or 'q' to quit): q
👋 Goodbye!
The app handles errors gracefully and keeps the loop running so you can try another city without restarting.
| Scenario | Message displayed |
|---|---|
| City name not recognised | ❌ Error: City not found |
| No internet / DNS failure | ❌ Error: Network error. Please check your connection... |
| API rate limit exceeded (429) | ❌ Error: Too many requests. Please try again later. |
| API server error (500) | ❌ Error: Internal server error. Please try again later. |
| Malformed API response | ❌ Error: Received invalid weather data from API |
| IP location unresolvable (VPN, NAT, etc.) | ❌ Error: Could not detect your location: <reason> |
| IP location API unavailable | ❌ Error: Network error. Please check your connection... |
? Type city name (or 'q' to quit): Atlantis
✖ Failed to retrieve weather data.
❌ Error: City not found
? Type city name (or 'q' to quit): _ ← loop continues
For -c / --current, errors print the message and exit with code 1:
$ weather -c
✖ Failed to detect location.
❌ Error: Could not detect your location: private range
Note: Pressing
Ctrl+Cat any prompt is handled safely and exits with a goodbye message.
┌─────────────────────────────────────────────┐
│ src/index.ts │
│ Entry point — shebang + calls runCLI() │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ src/cli.ts │
│ Commander setup — parses process.argv │
│ • -c, --current → getLocationByIP, exit │
│ • -u, --unit → save unit, exit │
│ • -s, --show-settings → print config, exit │
│ • --clear-default → delete config key │
│ • (no flag) → startApp() │
└──────────┬──────────────────────┬───────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ src/app.ts │ │ src/utils/config.ts │
│ │ │ │
│ startApp() │ │ Conf instance for │
│ • auto-detect │◄───│ reading/writing │
│ on first run │ │ defaultCity + unit │
│ • prompt loop │ │ to disk │
│ • save default │ └──────────────────────┘
│ • quit on 'q' │
└────────┬────────┘
│
┌────┴─────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ src/services/ │ │ src/ui/ │
│ weather.ts │ │ display.ts │
│ │ │ │
│ getWeatherData() │ │ displayWeather(...) │
│ (city, unit) │ │ displayError(...) │
│ 1. Sanitise input │ │ │
│ 2. Geocoding API │ │ Formats and prints │
│ → lat/lon │ │ coloured output to │
│ 3. → getWeatherBy │ │ stdout using chalk │
│ Coords(...) │ └──────────────────────┘
│ │
│ getWeatherByCoords() │
│ (lat, lon, name, │
│ unit) │
│ 1. Forecast API │
│ → temp + wind │
│ 2. Zod validation │
│ 3. Return result │
└──────────┬───────────┘
▲ │ imports schemas from
│ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ src/services/ │ │ src/types/ │
│ location.ts │ │ Response.ts │
│ │ │ │
│ getLocationByIP() │ │ GeocodingSchema │
│ 1. ip-api.com │ │ WeatherSchema (Zod) │
│ → lat/lon/city │ │ LocationSchema (Zod) │
│ 2. → getWeatherBy │ │ Inferred TS types │
│ Coords(lat,lon) │ └──────────────────────┘
└──────────────────────┘
src/index.tsis the binary entry point (shebang). It callsrunCLI()and pipes any unhandled errors toprocess.exit(1).src/cli.tsparsesprocess.argvwith Commander.-c, --current— reads the saved unit, starts a spinner, callsgetLocationByIP(unit)fromsrc/services/location.ts, displays weather withdisplayWeather, then exits. On error:displayError+process.exit(1).-u, --unit <value>— resolves shorthands (m/i) to full names, validates, saves to config, and exits.-s, --show-settings— reads the current default city and unit from config, prints them, and exits.--clear-default— deletes the default city from config and exits.- No flag — calls
startApp().
src/app.tsprints the welcome banner. If no default city is saved, it first prompts: "No saved city found — detect weather for your current location?". If the user accepts, a spinner fires,getLocationByIP(unit)is called, weather is displayed, and the user is offered to save the detected city as default.- After the optional auto-detect step, the app enters a
while (true)prompt loop powered byinquirer. On each iteration it reads the saved default city fromsrc/utils/config.tsand pre-fills the prompt. If the user enters a new city, a confirm prompt offers to save it as default. - If the user types
q(or pressesCtrl+C), the loop breaks cleanly. - Otherwise an
oraspinner starts. The savedunitis read from config and passed togetWeatherData(city, unit)insrc/services/weather.ts. - Inside
getWeatherData:- Input is sanitised (trimmed & lowercased).
- A request hits the Open-Meteo Geocoding API, validated with
GeocodingResponseSchemafromsrc/types/Response.ts. - If no results are returned, a
"City not found"error is thrown. - The resolved
latitude,longitude, and location label are passed togetWeatherByCoords.
- Inside
getWeatherByCoords(also called directly bygetLocationByIPto skip geocoding):- A request hits the Open-Meteo Forecast API — when
unitis"imperial",temperature_unit=fahrenheitandwind_speed_unit=mphare appended so the API returns values in the correct unit. - The response is validated with
WeatherSchemaand a plain{ location, temp, wind }object is returned.
- A request hits the Open-Meteo Forecast API — when
- Inside
getLocationByIP(used by-cand the startup auto-detect):- A request hits ip-api.com to resolve the caller's IP to
city,country,lat,lon. Thestatusfield is checked before Zod validation so"fail"responses (VPN, NAT, etc.) surface a clear error. lat/lonare passed directly togetWeatherByCoords— the geocoding step is skipped entirely since coordinates are already known.
- A request hits ip-api.com to resolve the caller's IP to
- Back in
src/app.tsorsrc/cli.ts, on success the spinner is marked succeeded anddisplayWeather(location, temp, wind, unit)fromsrc/ui/display.tsprints the result. - On any error, the spinner is marked failed and
displayErrorprints the message in red. In interactive mode the loop continues; in-cmode the process exits with code 1.
The project uses Jest with ts-jest for full TypeScript support. Tests are co-located with their source files under src/. The suite currently has 5 test files and 168 tests.
npm test
To check coverage:
npm run test:coverage
| Test file | What it tests |
|---|---|
src/cli.test.ts |
runCLI() — all flags (-c, -u, -s, --clear-default), aliases, invalid input, dispatch |
src/index.test.ts |
startApp() — prompts, spinner, unit threading, auto-detect flow, routing, edge cases |
src/services/location.test.ts |
getLocationByIP() — IP API happy path, status fail, Zod validation, HTTP/network errors, error propagation |
src/services/weather.test.ts |
getWeatherByCoords() and getWeatherData() — API calls, unit params, Zod validation, error branches |
src/ui/display.test.ts |
displayWeather() and displayError() — metric and imperial output formatting |
All external dependencies (axios, inquirer, chalk, ora, conf) are mocked in tests so no real network calls or disk writes are made.