From 2f162c66df693a1c5100fde97770a5298d0f6ba1 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 09:55:46 -0700 Subject: [PATCH 01/11] Add a prototype program that shows how to hit Open Meteo. --- package.json | 5 ++++ prototype.js | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 package.json create mode 100755 prototype.js diff --git a/package.json b/package.json new file mode 100644 index 0000000..97e6df0 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "yargs": "^18.1.0" + } +} diff --git a/prototype.js b/prototype.js new file mode 100755 index 0000000..d4d2c67 --- /dev/null +++ b/prototype.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node +// 2026-09-12 - Hyperling +/* Prototype mostly generated by Ask Brave from prompt: + * node.js script which has 8 location variables including name, lat, lon, and elevation. + * loops over the locations sending them to open-meteo requesting temperature min and max for 7 days, no hourly. + * uses units for USA such as fahrenheit, inches, mph. + * prints the location name and elevation with the average high and low for the week. + * */ +// Maybe AI is actually getting better than I thought. ;D + +const locations = [ + { name: "Flagstaff, AZ", lat: 35.1983, lon: -111.6513, elevation: 6903 }, + { name: "Tucson, AZ", lat: 32.2226, lon: -110.9747, elevation: 2490 }, + { name: "Prescott, AZ", lat: 34.5400, lon: -112.4685, elevation: 5374 }, + { name: "Frisco, CO", lat: 39.5744, lon: -106.0975, elevation: 9075 }, + { name: "Greer, AZ", lat: 34.0100, lon: -109.4587, elevation: 8356 }, + { name: "Sedona, AZ", lat: 34.8697, lon: -111.7610, elevation: 4350 }, + { name: "Ehrenberg, AZ", lat: 33.9897, lon: -114.3550, elevation: 333 }, +]; + +locations.sort((a, b) => a.elevation - b.elevation); + +const GREEN = "\x1b[32m"; +const RESET = "\x1b[0m"; + +function highlight(value, min, max) { + const n = parseFloat(value); + return (n >= min && n <= max) ? `${GREEN}${value}${RESET}` : value; +} + +async function getForecast(loc) { + const url = new URL("https://api.open-meteo.com/v1/forecast"); + url.searchParams.set("latitude", loc.lat); + url.searchParams.set("longitude", loc.lon); + url.searchParams.set("daily", "temperature_2m_max,temperature_2m_min"); + url.searchParams.set("temperature_unit", "fahrenheit"); + url.searchParams.set("precipitation_unit", "inch"); + url.searchParams.set("wind_speed_unit", "mph"); + url.searchParams.set("timezone", "auto"); + url.searchParams.set("forecast_days", "7"); + + const res = await fetch(url); + const data = await res.json(); + + const highs = data.daily.temperature_2m_max; + const lows = data.daily.temperature_2m_min; + + const avgHigh = (highs.reduce((a, b) => a + b, 0) / highs.length).toFixed(1); + const avgLow = (lows.reduce((a, b) => a + b, 0) / lows.length).toFixed(1); + + return { name: loc.name, elevation: loc.elevation, avgHigh, avgLow }; +} + +(async () => { + console.log("Location | Elev (ft) | Avg High (°F) | Avg Low (°F)"); + console.log("----------------------+-----------+---------------+------------"); + + for (const loc of locations) { + try { + const r = await getForecast(loc); + const hi = highlight(r.avgHigh, 60, 75); + const lo = highlight(r.avgLow, 40, 55); + console.log( + `${r.name.padEnd(20)} | ${String(r.elevation).padStart(9)} | ${hi.padStart(13)} | ${lo.padStart(12)}` + ); + } catch (e) { + console.log(`${loc.name}: error – ${e.message}`); + } + } +})(); From 1474956880960b8311f229861041e2abd0aa8c66 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 09:56:25 -0700 Subject: [PATCH 02/11] Commit my changes to the prototype to make it good for daily usage until the app is created. --- prototype.js | 147 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 134 insertions(+), 13 deletions(-) diff --git a/prototype.js b/prototype.js index d4d2c67..6c8b087 100755 --- a/prototype.js +++ b/prototype.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +"use strict"; // 2026-09-12 - Hyperling /* Prototype mostly generated by Ask Brave from prompt: * node.js script which has 8 location variables including name, lat, lon, and elevation. @@ -8,36 +9,148 @@ * */ // Maybe AI is actually getting better than I thought. ;D +/* AI Updates: + * - highlight highs between 60 and 75, highlight lows between 40 and 55 + * - order locations by elevation ascending + * */ + +/* User Updates + * - fix spacing in table + * - move temps to be right aligned + * - send elevation to API + * - new highlighting function with many other colors + * - added apparent (feels like) + * - added args, basically took over the program without any incremental commits + * */ + +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +const version = '0.0.1'; + +const days_fore_max = 16 +const days_fore_default = 10; +const days_past_default = 4; + +const argv = yargs(hideBin(process.argv)) + .option('days_fore', { + alias: 'f', + describe: 'Set forecast days', + type: 'number', + requiresArg: true, + }) + .option('days_past', { + alias: 'p', + describe: 'Set past days', + type: 'number', + requiresArg: true, + }) + .check((argv) => { + if (argv.f !== undefined && (isNaN(argv.f) || argv.f < 0)) { + throw new Error('Forecast days must be a non-negative number'); + } + if (argv.p !== undefined && (isNaN(argv.p) || argv.p < 0)) { + throw new Error('Past days must be a non-negative number'); + } + return true; + }) + //.demandOption(['timeout', 'debug']) // Optional: require these arguments + .strict() + .help() + .version(version) + .argv +; + +function max (value, upper) { + return (value > upper) ? upper : value; +} +const days_fore = max(argv.hasOwnProperty("days_fore") ? argv.days_fore : days_fore_default, days_fore_max); +const days_past = argv.hasOwnProperty("days_past") ? argv.days_past : days_past_default; + +const days = days_fore + days_past; + +console.log(`Running for ${days} days: ${days_fore} forecasted, ${days_past} past.`); + const locations = [ { name: "Flagstaff, AZ", lat: 35.1983, lon: -111.6513, elevation: 6903 }, + { name: "Flagstaff, ++", lat: 35.1983, lon: -111.6513, elevation: 8200 }, { name: "Tucson, AZ", lat: 32.2226, lon: -110.9747, elevation: 2490 }, { name: "Prescott, AZ", lat: 34.5400, lon: -112.4685, elevation: 5374 }, + { name: "Prescott, ++", lat: 34.5400, lon: -112.4685, elevation: 6500 }, { name: "Frisco, CO", lat: 39.5744, lon: -106.0975, elevation: 9075 }, + { name: "Frisco, ++", lat: 39.5744, lon: -106.0975, elevation: 9800 }, { name: "Greer, AZ", lat: 34.0100, lon: -109.4587, elevation: 8356 }, + { name: "Greer, ++", lat: 34.0100, lon: -109.4587, elevation: 9000 }, { name: "Sedona, AZ", lat: 34.8697, lon: -111.7610, elevation: 4350 }, { name: "Ehrenberg, AZ", lat: 33.9897, lon: -114.3550, elevation: 333 }, ]; - locations.sort((a, b) => a.elevation - b.elevation); -const GREEN = "\x1b[32m"; +const RED = "\x1b[31m"; +const GRN = "\x1b[32m"; +const YLW = "\x1b[33m"; +const BLU = "\x1b[34m"; +const MGN = "\x1b[35m"; +const CYN = "\x1b[36m"; +const WHT = "\x1b[37m"; const RESET = "\x1b[0m"; function highlight(value, min, max) { const n = parseFloat(value); - return (n >= min && n <= max) ? `${GREEN}${value}${RESET}` : value; + return (n >= min && n <= max) ? `${GRN}${value}${RESET}` : value; +} + +const pref_high_upper = 75; +const pref_high_lower = 60; +const pref_low_upper = 55; +const pref_low_lower = 40; + +const tolerance_light = 10; +const tolerance_heavy = 20; + +function check (value, min, max) { + const n = parseFloat(value); + + var CLR = ""; + + if (n >= max + tolerance_heavy) + CLR = MGN; + else if (n >= max + tolerance_light) + CLR = RED; + else if (n >= max) + CLR = YLW; + else if (n <= max && n >= min) + CLR = GRN; + else if (n <= min - tolerance_heavy) + CLR = WHT + else if (n <= min - tolerance_light) + CLR = CYN + else if (n <= min) + CLR = BLU; + else + CLR = RESET; + + return `${CLR}${value}${RESET}`; + //return (n >= min && n <= max) ? `${GRN}${value}${RESET}` : value; +} + +function pad(str, width) { + const visible = str.replace(/\x1b\[\d+m/g, '').length; + return ' '.repeat(Math.max(0, width - visible)) + str; } async function getForecast(loc) { const url = new URL("https://api.open-meteo.com/v1/forecast"); url.searchParams.set("latitude", loc.lat); url.searchParams.set("longitude", loc.lon); - url.searchParams.set("daily", "temperature_2m_max,temperature_2m_min"); + url.searchParams.set("elevation", loc.elevation * 0.3048); + url.searchParams.set("daily", "temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min"); url.searchParams.set("temperature_unit", "fahrenheit"); url.searchParams.set("precipitation_unit", "inch"); url.searchParams.set("wind_speed_unit", "mph"); url.searchParams.set("timezone", "auto"); - url.searchParams.set("forecast_days", "7"); + url.searchParams.set("forecast_days", days_fore); + url.searchParams.set("past_days", days_past); const res = await fetch(url); const data = await res.json(); @@ -48,21 +161,29 @@ async function getForecast(loc) { const avgHigh = (highs.reduce((a, b) => a + b, 0) / highs.length).toFixed(1); const avgLow = (lows.reduce((a, b) => a + b, 0) / lows.length).toFixed(1); - return { name: loc.name, elevation: loc.elevation, avgHigh, avgLow }; + const highs2 = data.daily.apparent_temperature_max; + const lows2 = data.daily.apparent_temperature_min; + + const avgHigh2 = (highs2.reduce((a, b) => a + b, 0) / highs2.length).toFixed(1); + const avgLow2 = (lows2.reduce((a, b) => a + b, 0) / lows2.length).toFixed(1); + + return { name: loc.name, elevation: loc.elevation, avgHigh, avgLow, avgHigh2, avgLow2 }; } (async () => { - console.log("Location | Elev (ft) | Avg High (°F) | Avg Low (°F)"); - console.log("----------------------+-----------+---------------+------------"); + console.log("Location | Elev (ft) | Avg High (°F) | Avg Low (°F) | App High (°F) | App Low (°F)"); + console.log("----------------------+-----------+---------------+--------------+---------------+-------------"); for (const loc of locations) { try { const r = await getForecast(loc); - const hi = highlight(r.avgHigh, 60, 75); - const lo = highlight(r.avgLow, 40, 55); - console.log( - `${r.name.padEnd(20)} | ${String(r.elevation).padStart(9)} | ${hi.padStart(13)} | ${lo.padStart(12)}` - ); + const hi = check(r.avgHigh, pref_high_lower, pref_high_upper); + const lo = check(r.avgLow, pref_low_lower, pref_low_upper); + const hi2 = check(r.avgHigh2, pref_high_lower, pref_high_upper); + const lo2 = check(r.avgLow2, pref_low_lower, pref_low_upper); + console.log( + `${pad(r.name, 21)} | ${pad(String(r.elevation), 9)} | ${pad(hi, 13)} | ${pad(lo, 12)} | ${pad(hi2, 13)} | ${pad(lo2, 12)}` + ); } catch (e) { console.log(`${loc.name}: error – ${e.message}`); } From 806dc5967ea054dca730436b885376831b903667 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 10:00:40 -0700 Subject: [PATCH 03/11] Add a gitignore and the package-lock.json. --- .gitignore | 34 ++++++++ package-lock.json | 203 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34c12be --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +build/ +out/ + +# Environment variables +.env +.env.local +.env.*.local +!.env.example + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Caches +.npm +.eslintcache +.cache/ +coverage/ + +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0192f38 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,203 @@ +{ + "name": "nomadic-weather", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "yargs": "^18.1.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + } + } +} From 1928fc011678b68054d54df314f42c3e295b9fdd Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 10:23:36 -0700 Subject: [PATCH 04/11] Change default day values and add a few more specific locations to the array. --- prototype.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/prototype.js b/prototype.js index 6c8b087..392d7f7 100755 --- a/prototype.js +++ b/prototype.js @@ -28,9 +28,9 @@ const { hideBin } = require('yargs/helpers'); const version = '0.0.1'; -const days_fore_max = 16 -const days_fore_default = 10; -const days_past_default = 4; +const days_fore_max = 16; +const days_fore_default = 12; +const days_past_default = 2; const argv = yargs(hideBin(process.argv)) .option('days_fore', { @@ -73,15 +73,22 @@ console.log(`Running for ${days} days: ${days_fore} forecasted, ${days_past} pas const locations = [ { name: "Flagstaff, AZ", lat: 35.1983, lon: -111.6513, elevation: 6903 }, - { name: "Flagstaff, ++", lat: 35.1983, lon: -111.6513, elevation: 8200 }, + { name: "Flagstaff: NG", lat: 35.1983, lon: -111.6513, elevation: 6931 }, + { name: "Flagstaff: ++", lat: 35.1983, lon: -111.6513, elevation: 8200 }, { name: "Tucson, AZ", lat: 32.2226, lon: -110.9747, elevation: 2490 }, + { name: "Tucson: NG", lat: 32.2226, lon: -110.9747, elevation: 2483 }, + { name: "Tucson: GV", lat: 32.2226, lon: -110.9747, elevation: 2371 }, + { name: "Tucson: ++", lat: 32.2226, lon: -110.9747, elevation: 2150 }, { name: "Prescott, AZ", lat: 34.5400, lon: -112.4685, elevation: 5374 }, - { name: "Prescott, ++", lat: 34.5400, lon: -112.4685, elevation: 6500 }, + { name: "Prescott: NG", lat: 34.5400, lon: -112.4685, elevation: 5496 }, + { name: "Prescott: ++", lat: 34.5400, lon: -112.4685, elevation: 6400 }, { name: "Frisco, CO", lat: 39.5744, lon: -106.0975, elevation: 9075 }, - { name: "Frisco, ++", lat: 39.5744, lon: -106.0975, elevation: 9800 }, + { name: "Frisco: NG", lat: 39.5744, lon: -106.0975, elevation: 9067 }, + { name: "Frisco: ++", lat: 39.5744, lon: -106.0975, elevation: 10200 }, { name: "Greer, AZ", lat: 34.0100, lon: -109.4587, elevation: 8356 }, - { name: "Greer, ++", lat: 34.0100, lon: -109.4587, elevation: 9000 }, + { name: "Greer: ++", lat: 34.0100, lon: -109.4587, elevation: 9000 }, { name: "Sedona, AZ", lat: 34.8697, lon: -111.7610, elevation: 4350 }, + { name: "Sedona: NG", lat: 34.8697, lon: -111.7610, elevation: 4420 }, { name: "Ehrenberg, AZ", lat: 33.9897, lon: -114.3550, elevation: 333 }, ]; locations.sort((a, b) => a.elevation - b.elevation); @@ -171,8 +178,8 @@ async function getForecast(loc) { } (async () => { - console.log("Location | Elev (ft) | Avg High (°F) | Avg Low (°F) | App High (°F) | App Low (°F)"); - console.log("----------------------+-----------+---------------+--------------+---------------+-------------"); + console.log("Location | Elev (ft) | Avg High (°F) | Avg Low (°F) | Feel High (°F) | Feel Low (°F)"); + console.log("----------------------+-----------+---------------+--------------+----------------+--------------"); for (const loc of locations) { try { @@ -182,7 +189,7 @@ async function getForecast(loc) { const hi2 = check(r.avgHigh2, pref_high_lower, pref_high_upper); const lo2 = check(r.avgLow2, pref_low_lower, pref_low_upper); console.log( - `${pad(r.name, 21)} | ${pad(String(r.elevation), 9)} | ${pad(hi, 13)} | ${pad(lo, 12)} | ${pad(hi2, 13)} | ${pad(lo2, 12)}` + `${pad(r.name, 21)} | ${pad(String(r.elevation), 9)} | ${pad(hi, 13)} | ${pad(lo, 12)} | ${pad(hi2, 14)} | ${pad(lo2, 13)}` ); } catch (e) { console.log(`${loc.name}: error – ${e.message}`); From 18939799098ddb57bf088af15f4588844382dcde Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 10:40:56 -0700 Subject: [PATCH 05/11] Finalize the locations, for now. :) --- prototype.js | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/prototype.js b/prototype.js index 392d7f7..be66070 100755 --- a/prototype.js +++ b/prototype.js @@ -69,27 +69,32 @@ const days_past = argv.hasOwnProperty("days_past") ? argv.days_past : days_past_ const days = days_fore + days_past; -console.log(`Running for ${days} days: ${days_fore} forecasted, ${days_past} past.`); +console.log(`Averaging for ${days} days: ${days_fore} forecasted, ${days_past} past.`); const locations = [ - { name: "Flagstaff, AZ", lat: 35.1983, lon: -111.6513, elevation: 6903 }, - { name: "Flagstaff: NG", lat: 35.1983, lon: -111.6513, elevation: 6931 }, + //{ name: "Flagstaff, AZ", lat: 35.1983, lon: -111.6513, elevation: 6903 }, + //{ name: "Flagstaff: NG", lat: 35.1983, lon: -111.6513, elevation: 6931 }, { name: "Flagstaff: ++", lat: 35.1983, lon: -111.6513, elevation: 8200 }, - { name: "Tucson, AZ", lat: 32.2226, lon: -110.9747, elevation: 2490 }, - { name: "Tucson: NG", lat: 32.2226, lon: -110.9747, elevation: 2483 }, - { name: "Tucson: GV", lat: 32.2226, lon: -110.9747, elevation: 2371 }, + //{ name: "Tucson, AZ", lat: 32.2226, lon: -110.9747, elevation: 2490 }, + //{ name: "Tucson: NG", lat: 32.2226, lon: -110.9747, elevation: 2483 }, + //{ name: "Tucson: GV", lat: 32.2226, lon: -110.9747, elevation: 2371 }, { name: "Tucson: ++", lat: 32.2226, lon: -110.9747, elevation: 2150 }, - { name: "Prescott, AZ", lat: 34.5400, lon: -112.4685, elevation: 5374 }, - { name: "Prescott: NG", lat: 34.5400, lon: -112.4685, elevation: 5496 }, + //{ name: "Prescott, AZ", lat: 34.5400, lon: -112.4685, elevation: 5374 }, + //{ name: "Prescott: NG", lat: 34.5400, lon: -112.4685, elevation: 5496 }, { name: "Prescott: ++", lat: 34.5400, lon: -112.4685, elevation: 6400 }, - { name: "Frisco, CO", lat: 39.5744, lon: -106.0975, elevation: 9075 }, - { name: "Frisco: NG", lat: 39.5744, lon: -106.0975, elevation: 9067 }, + //{ name: "Frisco, CO", lat: 39.5744, lon: -106.0975, elevation: 9075 }, + //{ name: "Frisco: NG", lat: 39.5744, lon: -106.0975, elevation: 9067 }, { name: "Frisco: ++", lat: 39.5744, lon: -106.0975, elevation: 10200 }, - { name: "Greer, AZ", lat: 34.0100, lon: -109.4587, elevation: 8356 }, + //{ name: "Greer, AZ", lat: 34.0100, lon: -109.4587, elevation: 8356 }, { name: "Greer: ++", lat: 34.0100, lon: -109.4587, elevation: 9000 }, - { name: "Sedona, AZ", lat: 34.8697, lon: -111.7610, elevation: 4350 }, - { name: "Sedona: NG", lat: 34.8697, lon: -111.7610, elevation: 4420 }, - { name: "Ehrenberg, AZ", lat: 33.9897, lon: -114.3550, elevation: 333 }, + //{ name: "Sedona, AZ", lat: 34.8697, lon: -111.7610, elevation: 4350 }, + { name: "Sedona: ++", lat: 34.8697, lon: -111.7610, elevation: 4500 }, + { name: "Cottonwood: ++", lat: 34.7392, lon: -112.0099, elevation: 3600 }, + { name: "Cherry: ++", lat: 34.5781, lon: -112.0677, elevation: 5300 }, + //{ name: "Sedona: NG", lat: 34.8697, lon: -111.7610, elevation: 4420 }, + //{ name: "Ehrenberg, AZ", lat: 33.9897, lon: -114.3550, elevation: 333 }, + { name: "Ehrenberg: ++", lat: 33.9897, lon: -114.3550, elevation: 333 }, + { name: "Quartzsite: ++", lat: 33.6763, lon: -114.2104, elevation: 888 }, ]; locations.sort((a, b) => a.elevation - b.elevation); @@ -178,8 +183,8 @@ async function getForecast(loc) { } (async () => { - console.log("Location | Elev (ft) | Avg High (°F) | Avg Low (°F) | Feel High (°F) | Feel Low (°F)"); - console.log("----------------------+-----------+---------------+--------------+----------------+--------------"); + console.log("Location | Elev (ft) | High (°F) | Low (°F) | FeelH (°F) | FeelL (°F)"); + console.log("----------------------+-----------+-----------+----------+------------+------------"); for (const loc of locations) { try { @@ -189,7 +194,7 @@ async function getForecast(loc) { const hi2 = check(r.avgHigh2, pref_high_lower, pref_high_upper); const lo2 = check(r.avgLow2, pref_low_lower, pref_low_upper); console.log( - `${pad(r.name, 21)} | ${pad(String(r.elevation), 9)} | ${pad(hi, 13)} | ${pad(lo, 12)} | ${pad(hi2, 14)} | ${pad(lo2, 13)}` + `${pad(r.name, 21)} | ${pad(String(r.elevation), 9)} | ${pad(hi, 9)} | ${pad(lo, 8)} | ${pad(hi2, 10)} | ${pad(lo2, 10)}` ); } catch (e) { console.log(`${loc.name}: error – ${e.message}`); From 7b9839b50ea92b9214a20b3e14eb7b453f0502cc Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 11:01:38 -0700 Subject: [PATCH 06/11] Add paramater so that provider calculates the elevation. This is the default in the API, but not with this program. --- prototype.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/prototype.js b/prototype.js index be66070..ab9fcd3 100755 --- a/prototype.js +++ b/prototype.js @@ -32,19 +32,26 @@ const days_fore_max = 16; const days_fore_default = 12; const days_past_default = 2; +const ft_to_m = 0.3048; + const argv = yargs(hideBin(process.argv)) .option('days_fore', { alias: 'f', - describe: 'Set forecast days', + describe: 'Set forecast days, such as "-f 10". Maximum of 16.', type: 'number', requiresArg: true, }) .option('days_past', { alias: 'p', - describe: 'Set past days', + describe: 'Set past days, such as "-p 3"', type: 'number', requiresArg: true, }) + .option('calc_elevation', { + alias: 'e', + describe: 'Set to calculate elevation at lat/long.', + type: 'boolean', + }) .check((argv) => { if (argv.f !== undefined && (isNaN(argv.f) || argv.f < 0)) { throw new Error('Forecast days must be a non-negative number'); @@ -66,10 +73,13 @@ function max (value, upper) { } const days_fore = max(argv.hasOwnProperty("days_fore") ? argv.days_fore : days_fore_default, days_fore_max); const days_past = argv.hasOwnProperty("days_past") ? argv.days_past : days_past_default; +const calc_elevation = argv.calc_elevation ? true : false; const days = days_fore + days_past; console.log(`Averaging for ${days} days: ${days_fore} forecasted, ${days_past} past.`); +if (calc_elevation) console.log("Calculating elevation rather than using provided values."); +else console.log("Using user-provided elevations when they exist."); const locations = [ //{ name: "Flagstaff, AZ", lat: 35.1983, lon: -111.6513, elevation: 6903 }, @@ -93,8 +103,8 @@ const locations = [ { name: "Cherry: ++", lat: 34.5781, lon: -112.0677, elevation: 5300 }, //{ name: "Sedona: NG", lat: 34.8697, lon: -111.7610, elevation: 4420 }, //{ name: "Ehrenberg, AZ", lat: 33.9897, lon: -114.3550, elevation: 333 }, - { name: "Ehrenberg: ++", lat: 33.9897, lon: -114.3550, elevation: 333 }, - { name: "Quartzsite: ++", lat: 33.6763, lon: -114.2104, elevation: 888 }, + { name: "Ehrenberg: ++", lat: 33.9897, lon: -114.3550, elevation: 300 }, + { name: "Quartzsite: ++", lat: 33.6763, lon: -114.2104, elevation: 900 }, ]; locations.sort((a, b) => a.elevation - b.elevation); @@ -155,7 +165,7 @@ async function getForecast(loc) { const url = new URL("https://api.open-meteo.com/v1/forecast"); url.searchParams.set("latitude", loc.lat); url.searchParams.set("longitude", loc.lon); - url.searchParams.set("elevation", loc.elevation * 0.3048); + if (!calc_elevation && loc.elevation) url.searchParams.set("elevation", loc.elevation * ft_to_m); url.searchParams.set("daily", "temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min"); url.searchParams.set("temperature_unit", "fahrenheit"); url.searchParams.set("precipitation_unit", "inch"); @@ -179,7 +189,9 @@ async function getForecast(loc) { const avgHigh2 = (highs2.reduce((a, b) => a + b, 0) / highs2.length).toFixed(1); const avgLow2 = (lows2.reduce((a, b) => a + b, 0) / lows2.length).toFixed(1); - return { name: loc.name, elevation: loc.elevation, avgHigh, avgLow, avgHigh2, avgLow2 }; + const elev = (!calc_elevation && loc.elevation) ? loc.elevation : (data.elevation / ft_to_m).toFixed(0); + + return { name: loc.name, elevation: elev, avgHigh, avgLow, avgHigh2, avgLow2 }; } (async () => { From 64e9abb85c38b4c7fb26cd5df03cf4fe6a0f1d27 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 12:06:20 -0700 Subject: [PATCH 07/11] Add script which allows the CLI tool to be run by local user. --- install.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 install.sh diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..c74acd0 --- /dev/null +++ b/install.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# 20260912 - Hyperling +# "Install" script to get this program runnable from anywhere on the system. + +DIR="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +LOCAL=~/bin + +mkdir -pv $LOCAL + +ln -svf "$DIR"/prototype.js $LOCAL/nomad +ln -svf "$DIR"/prototype.js $LOCAL/nomadic-weather From cde66dcd6796a59c8e7acc0c4517a360efa833b7 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 12:21:02 -0700 Subject: [PATCH 08/11] Change script to actually install the Node,js file, including in the global bin. --- install.sh | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index c74acd0..63d4c6e 100755 --- a/install.sh +++ b/install.sh @@ -2,10 +2,61 @@ # 20260912 - Hyperling # "Install" script to get this program runnable from anywhere on the system. +## Variables ## + DIR="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" LOCAL=~/bin +GLOBAL=/usr/local/bin -mkdir -pv $LOCAL +name_short="nomad" +name_long="nomadic-weather" +name_abs="weather-list" -ln -svf "$DIR"/prototype.js $LOCAL/nomad -ln -svf "$DIR"/prototype.js $LOCAL/nomadic-weather +## Functions ## + +## Parameters ## + +while getopts ":gslah" arg; do + case $arg in + g) global="Y" ;; + s) short="Y" ;; + l) long="Y" ;; + a) abstract="Y" ;; + h) usage ;; + *) echo "ERROR: Parameter $OPTARG was not recognized." && usage 1 ;; + esac +done + +## Processing ## + +name="" +if [[ "$short" == "Y" ]]; then + name="$name $name_short" +fi +if [[ "$long" == "Y" ]]; then + name="$name $name_long" +fi +if [[ "$abstract" == "Y" || ( -z "$short" && -z "$long" ) ]]; then + name="$name $name_abs" +fi + +cmd="" +dir="$LOCAL" +if [[ "$global" == "Y" ]]; then + dir="$GLOBAL" + cmd="sudo" + echo "Requesting sudo privileges... " + sudo echo "Success!" +fi + +## Main ## + +for prog in $name; do + mkdir -pv $dir + $cmd cp -v -svf "$DIR"/prototype.js $dir/$prog +done + +## Finish ## + +echo "Done!" +exit 0 From d70c5d30691fea148b408c6492ada4d7f8bfb6af Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 12:25:28 -0700 Subject: [PATCH 09/11] install, add a remove option. --- install.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 63d4c6e..f9961ed 100755 --- a/install.sh +++ b/install.sh @@ -16,12 +16,13 @@ name_abs="weather-list" ## Parameters ## -while getopts ":gslah" arg; do +while getopts ":gslarh" arg; do case $arg in g) global="Y" ;; s) short="Y" ;; l) long="Y" ;; a) abstract="Y" ;; + r) remove="Y" ;; h) usage ;; *) echo "ERROR: Parameter $OPTARG was not recognized." && usage 1 ;; esac @@ -41,10 +42,16 @@ if [[ "$abstract" == "Y" || ( -z "$short" && -z "$long" ) ]]; then fi cmd="" +if [[ -z "$remove" ]]; then + cmd="cp -v $DIR/prototype.js" +else + cmd="rm -v" +fi + dir="$LOCAL" if [[ "$global" == "Y" ]]; then dir="$GLOBAL" - cmd="sudo" + cmd="sudo $cmd" echo "Requesting sudo privileges... " sudo echo "Success!" fi @@ -53,7 +60,7 @@ fi for prog in $name; do mkdir -pv $dir - $cmd cp -v -svf "$DIR"/prototype.js $dir/$prog + $cmd $dir/$prog done ## Finish ## From bd02d4611a0b230daaec7e9774c9d5ae977647f5 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 12:48:22 -0700 Subject: [PATCH 10/11] Add usage as well as fix the global install to work properly with node_modules. --- install.sh | 59 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index f9961ed..b38f190 100755 --- a/install.sh +++ b/install.sh @@ -5,15 +5,47 @@ ## Variables ## DIR="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +PROG="$(basename -- "${BASH_SOURCE[0]}")" LOCAL=~/bin GLOBAL=/usr/local/bin +GLOBAL_SRC=/usr/local/src +name_local="prototype.js" name_short="nomad" name_long="nomadic-weather" name_abs="weather-list" ## Functions ## +function usage { + cat <<- EOF + $PROG [-h] [-g] [a|l|s] [-r] + + Install $name_local to the system. Can be local, global, or removed. + + Parameters) + h) Display this message. + g) Install globally ($GLOBAL), otherwise local ($LOCAL). + a) Abstract name, unrelated to being nomadic. ($name_abs) + l) Long name, the full name of the project. ($name_long) + s) Short name, the full name of the project. ($name_short) + r) Remove / uninstall the project according to the above flags. + + Examples) + - Install to the local user with the abstract name. + $PROG + - Install to all users with all names. + $PROG -alsg + - Remove all instances. + $PROG -alsr && $PROG -alsrg + + Happy weather watching! :) + + ~ Hyperling + EOF + exit $1 +} + ## Parameters ## while getopts ":gslarh" arg; do @@ -42,25 +74,34 @@ if [[ "$abstract" == "Y" || ( -z "$short" && -z "$long" ) ]]; then fi cmd="" -if [[ -z "$remove" ]]; then - cmd="cp -v $DIR/prototype.js" -else - cmd="rm -v" -fi - dir="$LOCAL" +src="$DIR" if [[ "$global" == "Y" ]]; then dir="$GLOBAL" - cmd="sudo $cmd" + cmd="sudo" echo "Requesting sudo privileges... " sudo echo "Success!" + mkdir -pv /usr/local/src + sudo cp -Rv "$DIR" $GLOBAL_SRC/ + src="$GLOBAL_SRC/`basename "$DIR"`" +fi + +if [[ -z "$remove" ]]; then + cmd="$cmd ln -svf $src/$name_local" +else + cmd="$cmd rm -v" fi ## Main ## +# Untested +#if [[ "$global" == "Y" && "$remove" == "Y" ]]; then +# sudo rm -rfv "$src" +#fi + for prog in $name; do - mkdir -pv $dir - $cmd $dir/$prog + mkdir -pv "$dir" + $cmd "$dir/$prog" done ## Finish ## From b3d3573faecb771f5b854cad1b5a8fb66851a981 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 12 Sep 2026 12:52:16 -0700 Subject: [PATCH 11/11] Move Node.js prototype to a separate cli folder. Works great! --- .gitignore => cli/.gitignore | 0 install.sh => cli/install.sh | 0 package-lock.json => cli/package-lock.json | 0 package.json => cli/package.json | 0 prototype.js => cli/prototype.js | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename .gitignore => cli/.gitignore (100%) rename install.sh => cli/install.sh (100%) rename package-lock.json => cli/package-lock.json (100%) rename package.json => cli/package.json (100%) rename prototype.js => cli/prototype.js (100%) diff --git a/.gitignore b/cli/.gitignore similarity index 100% rename from .gitignore rename to cli/.gitignore diff --git a/install.sh b/cli/install.sh similarity index 100% rename from install.sh rename to cli/install.sh diff --git a/package-lock.json b/cli/package-lock.json similarity index 100% rename from package-lock.json rename to cli/package-lock.json diff --git a/package.json b/cli/package.json similarity index 100% rename from package.json rename to cli/package.json diff --git a/prototype.js b/cli/prototype.js similarity index 100% rename from prototype.js rename to cli/prototype.js