tuiweather 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +30 -12
  2. package/dist/index.js +735 -289
  3. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -73193,8 +73193,143 @@ class PluginErrorBoundary extends import_react14.default.Component {
73193
73193
  }
73194
73194
  extend({ "time-to-first-draw": TimeToFirstDrawRenderable });
73195
73195
 
73196
+ // src/app/AppearanceApp.tsx
73197
+ var import_react40 = __toESM(require_react(), 1);
73198
+
73199
+ // src/theme/palette.ts
73200
+ var DARK_INK = {
73201
+ fg: "#c0caf5",
73202
+ fgDim: "#565f89",
73203
+ border: "#3b4261",
73204
+ surface: "#16161e"
73205
+ };
73206
+ var LIGHT_INK = {
73207
+ fg: "#343b58",
73208
+ fgDim: "#8990b3",
73209
+ border: "#a8b0d0",
73210
+ surface: "#f4f6fb"
73211
+ };
73212
+ var DAY_ACCENTS = {
73213
+ accent: "#2e7de9",
73214
+ ok: "#387068",
73215
+ warn: "#8c6c3e",
73216
+ danger: "#c64343",
73217
+ tempCold: "#007197",
73218
+ tempWarm: "#965027",
73219
+ rain: "#00807a"
73220
+ };
73221
+ var NIGHT_ACCENTS = {
73222
+ accent: "#7aa2f7",
73223
+ ok: "#9ece6a",
73224
+ warn: "#e0af68",
73225
+ danger: "#f7768e",
73226
+ tempCold: "#7dcfff",
73227
+ tempWarm: "#ff9e64",
73228
+ rain: "#41a6b5"
73229
+ };
73230
+ function parseHexColor(hex) {
73231
+ const m2 = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(hex.trim());
73232
+ const body = m2?.[1];
73233
+ if (!body)
73234
+ return null;
73235
+ const full = body.length === 3 ? body.split("").map((c) => c + c).join("") : body;
73236
+ const r = Number.parseInt(full.slice(0, 2), 16);
73237
+ const g2 = Number.parseInt(full.slice(2, 4), 16);
73238
+ const b2 = Number.parseInt(full.slice(4, 6), 16);
73239
+ return [r, g2, b2];
73240
+ }
73241
+ function relativeLuminance(hex) {
73242
+ const rgb = parseHexColor(hex);
73243
+ if (!rgb)
73244
+ return null;
73245
+ const lin = rgb.map((channel) => {
73246
+ const s = channel / 255;
73247
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
73248
+ });
73249
+ const [r, g2, b2] = lin;
73250
+ return 0.2126 * (r ?? 0) + 0.7152 * (g2 ?? 0) + 0.0722 * (b2 ?? 0);
73251
+ }
73252
+ function contrastRatio(a, b2) {
73253
+ const la = relativeLuminance(a);
73254
+ const lb = relativeLuminance(b2);
73255
+ if (la === null || lb === null)
73256
+ return 1;
73257
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
73258
+ return (hi + 0.05) / (lo + 0.05);
73259
+ }
73260
+ function isDarkBackground(hex) {
73261
+ if (!hex)
73262
+ return true;
73263
+ const lum = relativeLuminance(hex);
73264
+ if (lum === null)
73265
+ return true;
73266
+ return lum < 0.5;
73267
+ }
73268
+ function ensureContrast(color, bg2, minRatio) {
73269
+ if (contrastRatio(color, bg2) >= minRatio)
73270
+ return color;
73271
+ const toward = isDarkBackground(bg2) ? "#ffffff" : "#000000";
73272
+ const rgb = parseHexColor(color);
73273
+ const tgt = parseHexColor(toward);
73274
+ if (!rgb || !tgt)
73275
+ return color;
73276
+ for (let step = 1;step <= 20; step++) {
73277
+ const t2 = step / 20;
73278
+ const mixed = rgb.map((c, i) => Math.round(c + ((tgt[i] ?? c) - c) * t2)).map((c) => c.toString(16).padStart(2, "0")).join("");
73279
+ const candidate = `#${mixed}`;
73280
+ if (contrastRatio(candidate, bg2) >= minRatio)
73281
+ return candidate;
73282
+ }
73283
+ return toward;
73284
+ }
73285
+ var FOREGROUND_CONTRAST_FLOOR = 4.5;
73286
+ function buildPalette(theme, isDay, ink, terminalBackground) {
73287
+ const base = {
73288
+ ...ink === "dark" ? DARK_INK : LIGHT_INK,
73289
+ ...theme === "day" ? DAY_ACCENTS : theme === "night" ? NIGHT_ACCENTS : isDay ? DAY_ACCENTS : NIGHT_ACCENTS
73290
+ };
73291
+ const bg2 = terminalBackground ?? base.surface;
73292
+ return {
73293
+ ...base,
73294
+ fg: ensureContrast(base.fg, bg2, FOREGROUND_CONTRAST_FLOOR),
73295
+ fgDim: ensureContrast(base.fgDim, bg2, FOREGROUND_CONTRAST_FLOOR),
73296
+ warn: ensureContrast(base.warn, bg2, FOREGROUND_CONTRAST_FLOOR),
73297
+ danger: ensureContrast(base.danger, bg2, FOREGROUND_CONTRAST_FLOOR)
73298
+ };
73299
+ }
73300
+
73301
+ // src/theme/detect.ts
73302
+ var FALLBACK_APPEARANCE = { ink: "dark", background: null };
73303
+ function appearancesEqual(a, b2) {
73304
+ return a.ink === b2.ink && a.background === b2.background;
73305
+ }
73306
+ async function detectTerminalAppearance(query, timeoutMs = 300) {
73307
+ let timer;
73308
+ try {
73309
+ const colors = await Promise.race([
73310
+ query.getPalette({ timeout: timeoutMs }),
73311
+ new Promise((_2, reject) => {
73312
+ timer = setTimeout(() => reject(new Error("palette query timed out")), timeoutMs);
73313
+ })
73314
+ ]);
73315
+ const background = colors?.defaultBackground ?? null;
73316
+ return { ink: isDarkBackground(background) ? "dark" : "light", background };
73317
+ } catch {
73318
+ return FALLBACK_APPEARANCE;
73319
+ } finally {
73320
+ if (timer !== undefined)
73321
+ clearTimeout(timer);
73322
+ }
73323
+ }
73324
+ async function resolveTerminalAppearance(preference, query, timeoutMs = 300) {
73325
+ if (preference === "light" || preference === "dark") {
73326
+ return { ink: preference, background: null };
73327
+ }
73328
+ return detectTerminalAppearance(query, timeoutMs);
73329
+ }
73330
+
73196
73331
  // src/app/App.tsx
73197
- var import_react37 = __toESM(require_react(), 1);
73332
+ var import_react38 = __toESM(require_react(), 1);
73198
73333
 
73199
73334
  // src/components/DaylightBar.tsx
73200
73335
  var import_react18 = __toESM(require_react(), 1);
@@ -73409,108 +73544,6 @@ function truncateCells(text, width) {
73409
73544
 
73410
73545
  // src/theme/tokens.ts
73411
73546
  var import_react17 = __toESM(require_react(), 1);
73412
-
73413
- // src/theme/palette.ts
73414
- var DARK_INK = {
73415
- fg: "#c0caf5",
73416
- fgDim: "#565f89",
73417
- border: "#3b4261",
73418
- surface: "#16161e"
73419
- };
73420
- var LIGHT_INK = {
73421
- fg: "#343b58",
73422
- fgDim: "#8990b3",
73423
- border: "#a8b0d0",
73424
- surface: "#f4f6fb"
73425
- };
73426
- var DAY_ACCENTS = {
73427
- accent: "#2e7de9",
73428
- ok: "#387068",
73429
- warn: "#8c6c3e",
73430
- danger: "#c64343",
73431
- tempCold: "#007197",
73432
- tempWarm: "#965027",
73433
- rain: "#00807a"
73434
- };
73435
- var NIGHT_ACCENTS = {
73436
- accent: "#7aa2f7",
73437
- ok: "#9ece6a",
73438
- warn: "#e0af68",
73439
- danger: "#f7768e",
73440
- tempCold: "#7dcfff",
73441
- tempWarm: "#ff9e64",
73442
- rain: "#41a6b5"
73443
- };
73444
- function parseHexColor(hex) {
73445
- const m2 = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(hex.trim());
73446
- const body = m2?.[1];
73447
- if (!body)
73448
- return null;
73449
- const full = body.length === 3 ? body.split("").map((c) => c + c).join("") : body;
73450
- const r = Number.parseInt(full.slice(0, 2), 16);
73451
- const g2 = Number.parseInt(full.slice(2, 4), 16);
73452
- const b2 = Number.parseInt(full.slice(4, 6), 16);
73453
- return [r, g2, b2];
73454
- }
73455
- function relativeLuminance(hex) {
73456
- const rgb = parseHexColor(hex);
73457
- if (!rgb)
73458
- return null;
73459
- const lin = rgb.map((channel) => {
73460
- const s = channel / 255;
73461
- return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
73462
- });
73463
- const [r, g2, b2] = lin;
73464
- return 0.2126 * (r ?? 0) + 0.7152 * (g2 ?? 0) + 0.0722 * (b2 ?? 0);
73465
- }
73466
- function contrastRatio(a, b2) {
73467
- const la = relativeLuminance(a);
73468
- const lb = relativeLuminance(b2);
73469
- if (la === null || lb === null)
73470
- return 1;
73471
- const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
73472
- return (hi + 0.05) / (lo + 0.05);
73473
- }
73474
- function isDarkBackground(hex) {
73475
- if (!hex)
73476
- return true;
73477
- const lum = relativeLuminance(hex);
73478
- if (lum === null)
73479
- return true;
73480
- return lum < 0.5;
73481
- }
73482
- function ensureContrast(color, bg2, minRatio) {
73483
- if (contrastRatio(color, bg2) >= minRatio)
73484
- return color;
73485
- const toward = isDarkBackground(bg2) ? "#ffffff" : "#000000";
73486
- const rgb = parseHexColor(color);
73487
- const tgt = parseHexColor(toward);
73488
- if (!rgb || !tgt)
73489
- return color;
73490
- for (let step = 1;step <= 20; step++) {
73491
- const t2 = step / 20;
73492
- const mixed = rgb.map((c, i) => Math.round(c + ((tgt[i] ?? c) - c) * t2)).map((c) => c.toString(16).padStart(2, "0")).join("");
73493
- const candidate = `#${mixed}`;
73494
- if (contrastRatio(candidate, bg2) >= minRatio)
73495
- return candidate;
73496
- }
73497
- return toward;
73498
- }
73499
- var FOREGROUND_CONTRAST_FLOOR = 4.5;
73500
- function buildPalette(theme, isDay, ink, terminalBackground) {
73501
- const base = {
73502
- ...ink === "dark" ? DARK_INK : LIGHT_INK,
73503
- ...theme === "day" ? DAY_ACCENTS : theme === "night" ? NIGHT_ACCENTS : isDay ? DAY_ACCENTS : NIGHT_ACCENTS
73504
- };
73505
- const bg2 = terminalBackground ?? base.surface;
73506
- return {
73507
- ...base,
73508
- fg: ensureContrast(base.fg, bg2, FOREGROUND_CONTRAST_FLOOR),
73509
- fgDim: ensureContrast(base.fgDim, bg2, FOREGROUND_CONTRAST_FLOOR)
73510
- };
73511
- }
73512
-
73513
- // src/theme/tokens.ts
73514
73547
  var ThemeContext = import_react17.createContext(buildPalette("auto", true, "dark", null));
73515
73548
  function usePalette() {
73516
73549
  return import_react17.useContext(ThemeContext);
@@ -73890,13 +73923,14 @@ function StatLine({ parts, dim: dim2 }) {
73890
73923
  }
73891
73924
  var Hero = import_react20.memo(function Hero2({ obs, prefs, compact = false, mini = false }) {
73892
73925
  const palette = usePalette();
73926
+ const tempFg = lerpHex(palette.tempCold, palette.tempWarm, tempWarmthT(obs.temperatureC));
73893
73927
  if (mini) {
73894
73928
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
73895
73929
  flexDirection: "row",
73896
73930
  gap: 1,
73897
73931
  children: [
73898
73932
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
73899
- fg: palette.tempWarm,
73933
+ fg: tempFg,
73900
73934
  children: formatTemp(obs.temperatureC, prefs.temp)
73901
73935
  }, undefined, false, undefined, this),
73902
73936
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
@@ -73915,7 +73949,7 @@ var Hero = import_react20.memo(function Hero2({ obs, prefs, compact = false, min
73915
73949
  gap: 1,
73916
73950
  children: [
73917
73951
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
73918
- fg: palette.tempWarm,
73952
+ fg: tempFg,
73919
73953
  children: `${formatTemp(obs.temperatureC, prefs.temp)}`
73920
73954
  }, undefined, false, undefined, this),
73921
73955
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
@@ -73934,7 +73968,6 @@ var Hero = import_react20.memo(function Hero2({ obs, prefs, compact = false, min
73934
73968
  ]
73935
73969
  }, undefined, true, undefined, this);
73936
73970
  }
73937
- const tempFg = lerpHex(palette.tempCold, palette.tempWarm, tempWarmthT(obs.temperatureC));
73938
73971
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
73939
73972
  flexDirection: "column",
73940
73973
  flexShrink: 0,
@@ -74720,15 +74753,31 @@ function LocationPicker({
74720
74753
  }
74721
74754
  function SearchOverlay({ store, width, height }) {
74722
74755
  const searchLocations = store((s) => s.searchLocations);
74756
+ const [busy, setBusy] = import_react25.useState(false);
74757
+ const [actionError, setActionError] = import_react25.useState(undefined);
74723
74758
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(LocationPicker, {
74724
74759
  searchLocations,
74725
74760
  width,
74726
74761
  height,
74762
+ busy,
74763
+ actionError,
74764
+ onQueryChange: () => setActionError(undefined),
74727
74765
  onCancel: () => store.getState().setOverlayOpen(false),
74728
74766
  onSelect: (chosen) => {
74767
+ if (busy)
74768
+ return;
74769
+ setBusy(true);
74770
+ setActionError(undefined);
74729
74771
  const state = store.getState();
74730
74772
  const entry = buildLocationEntry(chosen, state.config.locations.map((loc) => loc.slug));
74731
- state.addLocation(entry).then(() => state.setOverlayOpen(false));
74773
+ state.addLocation(entry).then((ok) => {
74774
+ if (ok) {
74775
+ store.getState().setOverlayOpen(false);
74776
+ } else {
74777
+ setActionError(store.getState().lastActionError ?? "could not save config");
74778
+ setBusy(false);
74779
+ }
74780
+ });
74732
74781
  }
74733
74782
  }, undefined, false, undefined, this);
74734
74783
  }
@@ -94235,7 +94284,7 @@ class ProviderError extends Error {
94235
94284
 
94236
94285
  // src/lib/config/schema.ts
94237
94286
  var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
94238
- var SCHEMA_VERSION = 3;
94287
+ var SCHEMA_VERSION = 4;
94239
94288
  var locationSchema = exports_external.object({
94240
94289
  slug: exports_external.string().regex(SLUG_PATTERN),
94241
94290
  label: exports_external.string().min(1).max(80),
@@ -94261,23 +94310,15 @@ var tuiConfigSchema = exports_external.object({
94261
94310
  time_format: exports_external.enum(["12h", "24h", "auto"]).default("auto"),
94262
94311
  unit_prefs: exports_external.object(unitPrefsShape).prefault({}),
94263
94312
  refresh_minutes: exports_external.number().int().min(1).default(10),
94313
+ reduced_motion: exports_external.boolean().default(false),
94264
94314
  theme: exports_external.enum(["day", "night", "auto"]).default("auto"),
94315
+ ink: exports_external.enum(["auto", "light", "dark"]).default("auto"),
94265
94316
  provider: exports_external.enum(PROVIDER_IDS).default("openmeteo"),
94266
94317
  daily_days: exports_external.number().int().min(1).max(16).default(7),
94267
94318
  hourly_hours: exports_external.number().int().min(12).max(48).default(24),
94268
94319
  panels: panelsSchema.prefault({}),
94269
94320
  default_location: exports_external.string().optional(),
94270
94321
  locations: exports_external.array(locationSchema).prefault([])
94271
- }).superRefine((config2, ctx) => {
94272
- const target = config2.default_location;
94273
- if (target === undefined)
94274
- return;
94275
- if (!config2.locations.some((loc) => loc.slug === target)) {
94276
- ctx.addIssue({
94277
- code: "custom",
94278
- message: `default_location "${target}" does not match any [[locations]] slug`
94279
- });
94280
- }
94281
94322
  }).transform((config2) => {
94282
94323
  const legacy = config2.units;
94283
94324
  const unit_prefs = {
@@ -94304,7 +94345,7 @@ function migrateConfig(raw) {
94304
94345
  }
94305
94346
  const doc2 = { ...raw };
94306
94347
  const version2 = raw.schema_version;
94307
- if (version2 === 1 || version2 === 2) {
94348
+ if (version2 === 1 || version2 === 2 || version2 === 3) {
94308
94349
  doc2.schema_version = SCHEMA_VERSION;
94309
94350
  }
94310
94351
  const unitsField = doc2.units;
@@ -94368,7 +94409,12 @@ async function loadConfig(path4) {
94368
94409
  if (e.issues.some((i) => i.path.length === 0 || i.path[0] === "schema_version")) {
94369
94410
  issues.push("hint: bare keys must appear before any [table] or [[array]] headers in TOML");
94370
94411
  }
94371
- throw new ConfigError(`invalid config in ${target}: ${issues.length} issue(s)`, issues);
94412
+ const preview = issues.slice(0, 3).join(`
94413
+ `);
94414
+ const more = issues.length > 3 ? `
94415
+ ... and ${issues.length - 3} more` : "";
94416
+ throw new ConfigError(`invalid config in ${target}: ${issues.length} issue(s)
94417
+ ${preview}${more}`, issues);
94372
94418
  }
94373
94419
  return config2;
94374
94420
  }
@@ -94382,7 +94428,9 @@ function serialize(config2) {
94382
94428
  schema_version: config2.schema_version,
94383
94429
  time_format: config2.time_format,
94384
94430
  refresh_minutes: config2.refresh_minutes,
94431
+ reduced_motion: config2.reduced_motion,
94385
94432
  theme: config2.theme,
94433
+ ink: config2.ink,
94386
94434
  provider: config2.provider,
94387
94435
  daily_days: config2.daily_days,
94388
94436
  hourly_hours: config2.hourly_hours
@@ -94403,7 +94451,7 @@ async function saveConfig(config2, path4) {
94403
94451
  tuiConfigSchema.parse(config2);
94404
94452
  const target = path4 ?? defaultConfigPath();
94405
94453
  const dir = dirname3(target);
94406
- await mkdir2(dir, { recursive: true });
94454
+ await mkdir2(dir, { recursive: true, mode: 448 });
94407
94455
  const tmp = join6(dir, `${basename3(target)}.tmp-${process.pid}-${randomBytes2(8).toString("hex")}`);
94408
94456
  let created = false;
94409
94457
  try {
@@ -94436,19 +94484,99 @@ function sanitizeText(text, maxChars) {
94436
94484
  }
94437
94485
  return out.slice(0, maxChars);
94438
94486
  }
94439
- function errorReason(body, schema, maxChars = 200) {
94487
+ function errorReason(body, schema, maxChars = 200, extractor) {
94440
94488
  const parsed = schema.safeParse(body);
94441
94489
  if (!parsed.success)
94442
94490
  return;
94443
- const reason = parsed.data.reason;
94444
- if (typeof reason !== "string")
94491
+ const raw = extractor ? extractor(parsed.data) : parsed.data.reason;
94492
+ if (typeof raw !== "string")
94445
94493
  return;
94446
- return sanitizeText(reason, maxChars);
94494
+ return sanitizeText(raw, maxChars);
94495
+ }
94496
+ function causeSuffix(cause) {
94497
+ let raw = "";
94498
+ if (cause instanceof Error) {
94499
+ const inner = cause.cause;
94500
+ if (inner instanceof Error)
94501
+ raw = inner.message;
94502
+ else if (typeof inner === "string")
94503
+ raw = inner;
94504
+ else if (cause.message !== "fetch failed")
94505
+ raw = cause.message;
94506
+ } else if (typeof cause === "string") {
94507
+ raw = cause;
94508
+ } else if (cause !== null && typeof cause === "object" && "message" in cause && typeof cause.message === "string") {
94509
+ raw = cause.message;
94510
+ }
94511
+ const sanitized = sanitizeText(raw, 200).trim();
94512
+ if (!sanitized)
94513
+ return "";
94514
+ return `: ${sanitized}`;
94447
94515
  }
94448
94516
  function httpError(status, body, opts) {
94449
- const reason = errorReason(body, opts.schema, opts.maxChars ?? 200);
94517
+ const reason = errorReason(body, opts.schema, opts.maxChars ?? 200, opts.extractor);
94450
94518
  return new ProviderError(`${opts.providerId} ${opts.label} failed (HTTP ${status})${reason ? `: ${reason}` : ""}`, opts.providerId);
94451
94519
  }
94520
+ var PROVIDER_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
94521
+ function sizeCapError(providerId, label, cap) {
94522
+ return new ProviderError(`${providerId} ${label} response exceeded size limit (${cap} bytes)`, providerId);
94523
+ }
94524
+ async function readJsonCapped(res, opts) {
94525
+ const cap = opts.maxBytes ?? PROVIDER_RESPONSE_MAX_BYTES;
94526
+ const contentLength = res.headers.get("content-length");
94527
+ if (contentLength !== null) {
94528
+ const trimmed = contentLength.trim();
94529
+ if (/^\d+$/.test(trimmed)) {
94530
+ const parsed = Number(trimmed);
94531
+ if (Number.isSafeInteger(parsed) && parsed > cap) {
94532
+ try {
94533
+ await res.body?.cancel();
94534
+ } catch {}
94535
+ throw sizeCapError(opts.providerId, opts.label, cap);
94536
+ }
94537
+ }
94538
+ }
94539
+ let bytes;
94540
+ if (res.body) {
94541
+ const reader = res.body.getReader();
94542
+ const chunks = [];
94543
+ let total = 0;
94544
+ while (true) {
94545
+ const { done, value } = await reader.read();
94546
+ if (done)
94547
+ break;
94548
+ if (value) {
94549
+ total += value.byteLength;
94550
+ if (total > cap) {
94551
+ try {
94552
+ await reader.cancel();
94553
+ } catch {}
94554
+ throw sizeCapError(opts.providerId, opts.label, cap);
94555
+ }
94556
+ chunks.push(value);
94557
+ }
94558
+ }
94559
+ bytes = new Uint8Array(total);
94560
+ let offset = 0;
94561
+ for (const chunk of chunks) {
94562
+ bytes.set(chunk, offset);
94563
+ offset += chunk.byteLength;
94564
+ }
94565
+ } else {
94566
+ const text2 = await res.text();
94567
+ const encoded = new TextEncoder().encode(text2);
94568
+ if (encoded.byteLength > cap) {
94569
+ throw sizeCapError(opts.providerId, opts.label, cap);
94570
+ }
94571
+ bytes = encoded;
94572
+ }
94573
+ const text = new TextDecoder().decode(bytes);
94574
+ try {
94575
+ return JSON.parse(text);
94576
+ } catch (cause) {
94577
+ throw new ProviderError(`${opts.providerId} ${opts.label} returned a non-JSON body (HTTP ${res.status})`, opts.providerId, cause);
94578
+ }
94579
+ }
94452
94580
 
94453
94581
  // src/lib/providers/openmeteo/schemas.ts
94454
94582
  var LOCAL_NAIVE_TIME = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?$/;
@@ -94462,6 +94590,7 @@ var apiErrorBodySchema = exports_external.object({
94462
94590
  error: exports_external.literal(true),
94463
94591
  reason: exports_external.string().optional()
94464
94592
  });
94593
+ var MAX_REASON_CHARS = 200;
94465
94594
  var currentBlockSchema = exports_external.object({
94466
94595
  time: localNaiveTime,
94467
94596
  interval: exports_external.number().optional(),
@@ -94469,9 +94598,7 @@ var currentBlockSchema = exports_external.object({
94469
94598
  relative_humidity_2m: exports_external.number(),
94470
94599
  apparent_temperature: exports_external.number(),
94471
94600
  is_day: isDayStep,
94472
- precipitation: exports_external.number().nullable(),
94473
94601
  weather_code: exports_external.number(),
94474
- cloud_cover: exports_external.number().nullable(),
94475
94602
  pressure_msl: exports_external.number().nullable(),
94476
94603
  wind_speed_10m: exports_external.number(),
94477
94604
  wind_direction_10m: exports_external.number(),
@@ -94525,11 +94652,9 @@ var dailyBlockSchema = exports_external.object({
94525
94652
  temperature_2m_min: numArray,
94526
94653
  precipitation_sum: numArray,
94527
94654
  precipitation_probability_max: numArray.optional(),
94528
- uv_index_max: numArray.optional(),
94529
94655
  sunrise: strOrNullArray.optional(),
94530
94656
  sunset: strOrNullArray.optional(),
94531
- wind_speed_10m_max: numArray.optional(),
94532
- wind_gusts_10m_max: numArray.optional()
94657
+ wind_speed_10m_max: numArray.optional()
94533
94658
  }).superRefine((block, ctx) => {
94534
94659
  const n = block.time.length;
94535
94660
  for (const [key, value] of Object.entries(block)) {
@@ -94585,10 +94710,15 @@ function buildGeocodingUrl(query, count = 8) {
94585
94710
  });
94586
94711
  return `${GEOCODING_ENDPOINT}?${params.toString()}`;
94587
94712
  }
94588
- function parseGeocodingResponse(body) {
94713
+ function parseGeocodingResponse(body, status = 200) {
94589
94714
  const errorParsed = apiErrorBodySchema.safeParse(body);
94590
94715
  if (errorParsed.success) {
94591
- throw new ProviderError(`openmeteo geocoding failed: ${sanitizeText(errorParsed.data.reason ?? "unknown error", 200)}`, "openmeteo");
94716
+ throw httpError(status, body, {
94717
+ label: "geocoding",
94718
+ providerId: "openmeteo",
94719
+ schema: apiErrorBodySchema,
94720
+ maxChars: MAX_REASON_CHARS
94721
+ });
94592
94722
  }
94593
94723
  const parsed = geocodingResponseSchema.safeParse(body);
94594
94724
  if (!parsed.success) {
@@ -94613,19 +94743,22 @@ async function searchLocations(query, count = 8) {
94613
94743
  signal: AbortSignal.timeout(TIMEOUT_MS)
94614
94744
  });
94615
94745
  } catch (cause) {
94616
- throw new ProviderError("openmeteo geocoding request failed before an HTTP response", "openmeteo", cause);
94746
+ throw new ProviderError(`openmeteo geocoding request failed before an HTTP response${causeSuffix(cause)}`, "openmeteo", cause);
94617
94747
  }
94618
94748
  let body;
94619
94749
  try {
94620
- body = await res.json();
94750
+ body = await readJsonCapped(res, { providerId: "openmeteo", label: "geocoding" });
94621
94751
  } catch (cause) {
94752
+ if (cause instanceof ProviderError)
94753
+ throw cause;
94622
94754
  throw new ProviderError(`openmeteo geocoding returned a non-JSON body (HTTP ${res.status})`, "openmeteo", cause);
94623
94755
  }
94624
94756
  if (!res.ok || errorReason(body, apiErrorBodySchema) !== undefined) {
94625
94757
  throw httpError(res.status, body, {
94626
94758
  label: "geocoding",
94627
94759
  providerId: "openmeteo",
94628
- schema: apiErrorBodySchema
94760
+ schema: apiErrorBodySchema,
94761
+ maxChars: MAX_REASON_CHARS
94629
94762
  });
94630
94763
  }
94631
94764
  return parseGeocodingResponse(body);
@@ -94633,7 +94766,7 @@ async function searchLocations(query, count = 8) {
94633
94766
  // package.json
94634
94767
  var package_default2 = {
94635
94768
  name: "tuiweather",
94636
- version: "0.3.7",
94769
+ version: "0.4.0",
94637
94770
  description: "Keyboard-driven terminal weather app powered by Open-Meteo",
94638
94771
  license: "MIT",
94639
94772
  type: "module",
@@ -94671,7 +94804,8 @@ var package_default2 = {
94671
94804
  lint: "biome check .",
94672
94805
  fmt: "biome check --write .",
94673
94806
  test: "bun test",
94674
- prepublishOnly: "bun run typecheck && bun run lint && bun run test && bun run build"
94807
+ verify: "bun run typecheck && bun run lint && bun run test && bun run build",
94808
+ prepublishOnly: "bun run verify"
94675
94809
  },
94676
94810
  dependencies: {
94677
94811
  "@opentui/core": "^0.5.9",
@@ -94903,11 +95037,9 @@ function normalizeCurrent(obs) {
94903
95037
  windDirectionDeg: quantityValue(obs.windDirection) ?? 0,
94904
95038
  windGustKmh: obsWindKmh(obs.windGust),
94905
95039
  pressureHpa: obsPressureHpa(obs.seaLevelPressure) ?? obsPressureHpa(obs.barometricPressure),
94906
- cloudCoverPct: null,
94907
95040
  dewPointC: obsTempC(obs.dewpoint),
94908
95041
  visibilityM: obsVisibilityM(obs.visibility),
94909
95042
  uvIndex: null,
94910
- precipLast1hMm: null,
94911
95043
  isDay: !icon.includes("/night/")
94912
95044
  };
94913
95045
  }
@@ -94972,11 +95104,9 @@ function normalizeDaily(periods, forecastDays) {
94972
95104
  quantityValue(group.day?.probabilityOfPrecipitation),
94973
95105
  quantityValue(group.night?.probabilityOfPrecipitation)
94974
95106
  ]),
94975
- uvIndexMax: null,
94976
95107
  sunriseUtc: null,
94977
95108
  sunsetUtc: null,
94978
- windSpeedMaxKmh: speeds.length > 0 ? Math.max(...speeds) : null,
94979
- windGustMaxKmh: null
95109
+ windSpeedMaxKmh: speeds.length > 0 ? Math.max(...speeds) : null
94980
95110
  });
94981
95111
  }
94982
95112
  return daily;
@@ -95075,6 +95205,26 @@ var API_ROOT = "https://api.weather.gov";
95075
95205
  var ALLOWED_HOST = new URL(API_ROOT).host;
95076
95206
  var TIMEOUT_MS2 = 1e4;
95077
95207
  var MAX_DETAIL_CHARS = 200;
95208
+ var NWS_METADATA_TTL_MS = 24 * 60 * 60 * 1000;
95209
+ var NWS_METADATA_MAX_ENTRIES = 64;
95210
+ function nwsProblemReason(data) {
95211
+ const v2 = data;
95212
+ const candidate = v2.detail ?? v2.title;
95213
+ return typeof candidate === "string" ? candidate : undefined;
95214
+ }
95215
+ function metadataKey(location) {
95216
+ return `${location.latitude.toFixed(4)}:${location.longitude.toFixed(4)}`;
95217
+ }
95218
+ var nwsMetadataMemo = new Map;
95219
+ var pendingForecasts = new Map;
95220
+ function evictOldestIfNeeded() {
95221
+ while (nwsMetadataMemo.size > NWS_METADATA_MAX_ENTRIES) {
95222
+ const oldest = nwsMetadataMemo.keys().next().value;
95223
+ if (oldest === undefined)
95224
+ break;
95225
+ nwsMetadataMemo.delete(oldest);
95226
+ }
95227
+ }
95078
95228
  var NWS_HEADERS = {
95079
95229
  "User-Agent": NWS_USER_AGENT,
95080
95230
  Accept: "application/geo+json"
@@ -95082,13 +95232,6 @@ var NWS_HEADERS = {
95082
95232
  function buildPointsUrl(location) {
95083
95233
  return `${API_ROOT}/points/${location.latitude},${location.longitude}`;
95084
95234
  }
95085
- function problemDetail(body) {
95086
- const parsed = nwsProblemSchema.safeParse(body);
95087
- if (!parsed.success)
95088
- return;
95089
- const detail = parsed.data.detail ?? parsed.data.title;
95090
- return detail === undefined ? undefined : sanitizeText(detail, MAX_DETAIL_CHARS);
95091
- }
95092
95235
  async function getJson(url2, label) {
95093
95236
  let target;
95094
95237
  try {
@@ -95101,19 +95244,45 @@ async function getJson(url2, label) {
95101
95244
  }
95102
95245
  let res;
95103
95246
  try {
95104
- res = await fetch(url2, { headers: NWS_HEADERS, signal: AbortSignal.timeout(TIMEOUT_MS2) });
95247
+ res = await fetch(url2, {
95248
+ headers: NWS_HEADERS,
95249
+ signal: AbortSignal.timeout(TIMEOUT_MS2),
95250
+ redirect: "error"
95251
+ });
95105
95252
  } catch (cause) {
95106
- throw new ProviderError(`nws ${label} request failed before an HTTP response`, NWS_PROVIDER_ID, cause);
95253
+ const message = cause instanceof Error ? cause.message : String(cause);
95254
+ if (message.toLowerCase().includes("redirect")) {
95255
+ throw new ProviderError(`nws ${label} redirected off-host`, NWS_PROVIDER_ID, cause);
95256
+ }
95257
+ throw new ProviderError(`nws ${label} request failed before an HTTP response${causeSuffix(cause)}`, NWS_PROVIDER_ID, cause);
95258
+ }
95259
+ if (res.url) {
95260
+ let final;
95261
+ try {
95262
+ final = new URL(res.url);
95263
+ } catch {
95264
+ throw new ProviderError(`nws ${label} redirected off-host`, NWS_PROVIDER_ID);
95265
+ }
95266
+ if (final.protocol !== "https:" || final.host !== ALLOWED_HOST) {
95267
+ throw new ProviderError(`nws ${label} redirected off-host`, NWS_PROVIDER_ID);
95268
+ }
95107
95269
  }
95108
95270
  let body;
95109
95271
  try {
95110
- body = await res.json();
95272
+ body = await readJsonCapped(res, { providerId: NWS_PROVIDER_ID, label });
95111
95273
  } catch (cause) {
95274
+ if (cause instanceof ProviderError)
95275
+ throw cause;
95112
95276
  throw new ProviderError(`nws ${label} returned a non-JSON body (HTTP ${res.status})`, NWS_PROVIDER_ID, cause);
95113
95277
  }
95114
95278
  if (!res.ok) {
95115
- const detail = problemDetail(body);
95116
- throw new ProviderError(`nws ${label} failed (HTTP ${res.status})${detail ? `: ${detail}` : ""}`, NWS_PROVIDER_ID);
95279
+ throw httpError(res.status, body, {
95280
+ label,
95281
+ providerId: NWS_PROVIDER_ID,
95282
+ schema: nwsProblemSchema,
95283
+ maxChars: MAX_DETAIL_CHARS,
95284
+ extractor: nwsProblemReason
95285
+ });
95117
95286
  }
95118
95287
  return body;
95119
95288
  }
@@ -95125,24 +95294,57 @@ function parseResponse(schema, body, label) {
95125
95294
  return parsed.data;
95126
95295
  }
95127
95296
  async function fetchForecast(location, window2) {
95128
- const points = parseResponse(pointsResponseSchema, await getJson(buildPointsUrl(location), "points"), "points");
95129
- const [hourlyBody, dailyBody, stationsBody] = await Promise.all([
95130
- getJson(points.properties.forecastHourly, "hourly forecast"),
95131
- getJson(points.properties.forecast, "daily forecast"),
95132
- getJson(points.properties.observationStations, "observation stations")
95133
- ]);
95134
- const stations = parseResponse(stationsResponseSchema, stationsBody, "observation stations");
95135
- const station = stations.features[0];
95136
- if (station === undefined) {
95137
- throw new ProviderError("nws observation stations list is empty; cannot fetch a current observation", NWS_PROVIDER_ID);
95138
- }
95139
- const obs = parseResponse(observationResponseSchema, await getJson(`${station.id}/observations/latest`, "latest observation"), "latest observation");
95140
- return normalizeNwsForecast({
95141
- points: points.properties,
95142
- hourly: parseResponse(forecastResponseSchema2, hourlyBody, "hourly forecast").properties.periods,
95143
- daily: parseResponse(forecastResponseSchema2, dailyBody, "daily forecast").properties.periods,
95144
- obs: obs.properties
95145
- }, location, window2);
95297
+ const key = metadataKey(location);
95298
+ const pendingKey = `${key}|${window2?.forecastDays ?? "*"}|${window2?.forecastHours ?? "*"}`;
95299
+ const pending = pendingForecasts.get(pendingKey);
95300
+ if (pending)
95301
+ return pending;
95302
+ const task = (async () => {
95303
+ const cached2 = nwsMetadataMemo.get(key);
95304
+ if (cached2 && Date.now() < cached2.expiresAt) {
95305
+ const meta4 = cached2.value;
95306
+ const [hourlyBody2, dailyBody2, obsBody] = await Promise.all([
95307
+ getJson(meta4.points.forecastHourly, "hourly forecast"),
95308
+ getJson(meta4.points.forecast, "daily forecast"),
95309
+ getJson(`${meta4.stationId}/observations/latest`, "latest observation")
95310
+ ]);
95311
+ return normalizeNwsForecast({
95312
+ points: meta4.points,
95313
+ hourly: parseResponse(forecastResponseSchema2, hourlyBody2, "hourly forecast").properties.periods,
95314
+ daily: parseResponse(forecastResponseSchema2, dailyBody2, "daily forecast").properties.periods,
95315
+ obs: parseResponse(observationResponseSchema, obsBody, "latest observation").properties
95316
+ }, location, window2);
95317
+ }
95318
+ if (cached2)
95319
+ nwsMetadataMemo.delete(key);
95320
+ const points = parseResponse(pointsResponseSchema, await getJson(buildPointsUrl(location), "points"), "points");
95321
+ const [hourlyBody, dailyBody, stationsBody] = await Promise.all([
95322
+ getJson(points.properties.forecastHourly, "hourly forecast"),
95323
+ getJson(points.properties.forecast, "daily forecast"),
95324
+ getJson(points.properties.observationStations, "observation stations")
95325
+ ]);
95326
+ const stations = parseResponse(stationsResponseSchema, stationsBody, "observation stations");
95327
+ const station = stations.features[0];
95328
+ if (station === undefined) {
95329
+ throw new ProviderError("nws observation stations list is empty; cannot fetch a current observation", NWS_PROVIDER_ID);
95330
+ }
95331
+ const obs = parseResponse(observationResponseSchema, await getJson(`${station.id}/observations/latest`, "latest observation"), "latest observation");
95332
+ const meta3 = { points: points.properties, stationId: station.id };
95333
+ nwsMetadataMemo.set(key, { value: meta3, expiresAt: Date.now() + NWS_METADATA_TTL_MS });
95334
+ evictOldestIfNeeded();
95335
+ return normalizeNwsForecast({
95336
+ points: points.properties,
95337
+ hourly: parseResponse(forecastResponseSchema2, hourlyBody, "hourly forecast").properties.periods,
95338
+ daily: parseResponse(forecastResponseSchema2, dailyBody, "daily forecast").properties.periods,
95339
+ obs: obs.properties
95340
+ }, location, window2);
95341
+ })();
95342
+ pendingForecasts.set(pendingKey, task);
95343
+ try {
95344
+ return await task;
95345
+ } finally {
95346
+ pendingForecasts.delete(pendingKey);
95347
+ }
95146
95348
  }
95147
95349
  var nwsProvider = {
95148
95350
  id: NWS_PROVIDER_ID,
@@ -95161,9 +95363,7 @@ var aqResponseSchema = exports_external.object({
95161
95363
  current: exports_external.object({
95162
95364
  time: localNaiveTime2,
95163
95365
  interval: exports_external.number().optional(),
95164
- us_aqi: exports_external.number().nullable().optional(),
95165
- pm2_5: exports_external.number().nullable().optional(),
95166
- ozone: exports_external.number().nullable().optional()
95366
+ us_aqi: exports_external.number().nullable().optional()
95167
95367
  }).passthrough(),
95168
95368
  current_units: exports_external.unknown().optional()
95169
95369
  }).passthrough();
@@ -95171,7 +95371,7 @@ function buildAirQualityUrl(location) {
95171
95371
  const params = new URLSearchParams({
95172
95372
  latitude: String(location.latitude),
95173
95373
  longitude: String(location.longitude),
95174
- current: ["us_aqi", "pm2_5", "ozone"].join(",")
95374
+ current: "us_aqi"
95175
95375
  });
95176
95376
  return `${AIR_QUALITY_ENDPOINT}?${params.toString()}`;
95177
95377
  }
@@ -95179,7 +95379,8 @@ function httpErrorFor(status, body) {
95179
95379
  return httpError(status, body, {
95180
95380
  label: "air-quality",
95181
95381
  providerId: "openmeteo",
95182
- schema: apiErrorBodySchema
95382
+ schema: apiErrorBodySchema,
95383
+ maxChars: MAX_REASON_CHARS
95183
95384
  });
95184
95385
  }
95185
95386
  function normalizeAirQuality(data) {
@@ -95187,8 +95388,6 @@ function normalizeAirQuality(data) {
95187
95388
  const observedAtUtc = new Date(Date.parse(`${data.current.time}Z`) - offsetMs).toISOString();
95188
95389
  return {
95189
95390
  usAqi: data.current.us_aqi ?? null,
95190
- pm25UgM3: data.current.pm2_5 ?? null,
95191
- ozoneUgM3: data.current.ozone ?? null,
95192
95391
  observedAtUtc
95193
95392
  };
95194
95393
  }
@@ -95203,8 +95402,10 @@ async function fetchAirQuality(location) {
95203
95402
  }
95204
95403
  let body;
95205
95404
  try {
95206
- body = await res.json();
95405
+ body = await readJsonCapped(res, { providerId: "openmeteo", label: "air-quality" });
95207
95406
  } catch (cause) {
95407
+ if (cause instanceof ProviderError)
95408
+ throw cause;
95208
95409
  throw new ProviderError(`openmeteo air-quality returned a non-JSON body (HTTP ${res.status})`, "openmeteo", cause);
95209
95410
  }
95210
95411
  if (!res.ok || errorReason(body, apiErrorBodySchema) !== undefined) {
@@ -95296,11 +95497,9 @@ function normalizeForecast(data, locationOverride) {
95296
95497
  windDirectionDeg: cur.wind_direction_10m,
95297
95498
  windGustKmh: cur.wind_gusts_10m ?? null,
95298
95499
  pressureHpa: cur.pressure_msl ?? null,
95299
- cloudCoverPct: cur.cloud_cover ?? null,
95300
95500
  dewPointC: cur.dew_point_2m ?? null,
95301
95501
  visibilityM: null,
95302
95502
  uvIndex: null,
95303
- precipLast1hMm: cur.precipitation ?? null,
95304
95503
  isDay: coerceIsDay(cur.is_day)
95305
95504
  };
95306
95505
  const minutely15 = normalizeMinutely(data.minutely_15, toUtc);
@@ -95342,11 +95541,9 @@ function normalizeForecast(data, locationOverride) {
95342
95541
  tempMaxC: requireNum(d2.temperature_2m_max, i, "daily.temperature_2m_max"),
95343
95542
  precipSumMm: requireNum(d2.precipitation_sum, i, "daily.precipitation_sum"),
95344
95543
  precipProbabilityMaxPct: optNum(d2.precipitation_probability_max, i),
95345
- uvIndexMax: optNum(d2.uv_index_max, i),
95346
95544
  sunriseUtc: typeof sunrise === "string" ? toUtc(sunrise) : null,
95347
95545
  sunsetUtc: typeof sunset === "string" ? toUtc(sunset) : null,
95348
- windSpeedMaxKmh: optNum(d2.wind_speed_10m_max, i),
95349
- windGustMaxKmh: optNum(d2.wind_gusts_10m_max, i)
95546
+ windSpeedMaxKmh: optNum(d2.wind_speed_10m_max, i)
95350
95547
  });
95351
95548
  }
95352
95549
  return {
@@ -95383,9 +95580,7 @@ var CURRENT_VARIABLES = [
95383
95580
  "relative_humidity_2m",
95384
95581
  "apparent_temperature",
95385
95582
  "is_day",
95386
- "precipitation",
95387
95583
  "weather_code",
95388
- "cloud_cover",
95389
95584
  "pressure_msl",
95390
95585
  "wind_speed_10m",
95391
95586
  "wind_direction_10m",
@@ -95413,11 +95608,9 @@ var DAILY_VARIABLES = [
95413
95608
  "temperature_2m_min",
95414
95609
  "precipitation_sum",
95415
95610
  "precipitation_probability_max",
95416
- "uv_index_max",
95417
95611
  "sunrise",
95418
95612
  "sunset",
95419
- "wind_speed_10m_max",
95420
- "wind_gusts_10m_max"
95613
+ "wind_speed_10m_max"
95421
95614
  ];
95422
95615
  function buildForecastUrl(location, opts = {}) {
95423
95616
  const params = new URLSearchParams({
@@ -95430,7 +95623,6 @@ function buildForecastUrl(location, opts = {}) {
95430
95623
  timezone: "auto",
95431
95624
  timeformat: "iso8601",
95432
95625
  forecast_days: String(opts.forecastDays ?? 3),
95433
- past_hours: String(opts.pastHours ?? 1),
95434
95626
  past_minutely_15: String(opts.pastMinutely15 ?? 8),
95435
95627
  forecast_minutely_15: String(opts.forecastMinutely15 ?? 12)
95436
95628
  });
@@ -95443,7 +95635,8 @@ function httpErrorFor2(status, body) {
95443
95635
  return httpError(status, body, {
95444
95636
  label: "forecast",
95445
95637
  providerId: OPENMETEO_PROVIDER_ID,
95446
- schema: apiErrorBodySchema
95638
+ schema: apiErrorBodySchema,
95639
+ maxChars: MAX_REASON_CHARS
95447
95640
  });
95448
95641
  }
95449
95642
  async function fetchForecast2(location, opts = {}) {
@@ -95453,12 +95646,17 @@ async function fetchForecast2(location, opts = {}) {
95453
95646
  signal: AbortSignal.timeout(TIMEOUT_MS4)
95454
95647
  });
95455
95648
  } catch (cause) {
95456
- throw new ProviderError("openmeteo forecast request failed before an HTTP response", OPENMETEO_PROVIDER_ID, cause);
95649
+ throw new ProviderError(`openmeteo forecast request failed before an HTTP response${causeSuffix(cause)}`, OPENMETEO_PROVIDER_ID, cause);
95457
95650
  }
95458
95651
  let body;
95459
95652
  try {
95460
- body = await res.json();
95653
+ body = await readJsonCapped(res, {
95654
+ providerId: OPENMETEO_PROVIDER_ID,
95655
+ label: "forecast"
95656
+ });
95461
95657
  } catch (cause) {
95658
+ if (cause instanceof ProviderError)
95659
+ throw cause;
95462
95660
  throw new ProviderError(`openmeteo forecast returned a non-JSON body (HTTP ${res.status})`, OPENMETEO_PROVIDER_ID, cause);
95463
95661
  }
95464
95662
  if (!res.ok || errorReason(body, apiErrorBodySchema) !== undefined) {
@@ -95483,12 +95681,14 @@ function selectProvider(id) {
95483
95681
 
95484
95682
  // src/lib/weather/cache.ts
95485
95683
  import { createHash, randomBytes as randomBytes3 } from "node:crypto";
95486
- import { chmod as chmod2, mkdir as mkdir3, open as open4, readFile as readFile3, rename as rename3, rm, unlink as unlink3 } from "node:fs/promises";
95684
+ import { chmod as chmod2, mkdir as mkdir3, open as open4, readdir, readFile as readFile3, rename as rename3, rm, stat as stat2, unlink as unlink3 } from "node:fs/promises";
95487
95685
  import { homedir as homedir2 } from "node:os";
95488
95686
  import { join as join7 } from "node:path";
95687
+ var CACHE_SCHEMA_VERSION = 2;
95489
95688
  var DEFAULT_MAX_AGE_MINUTES = 10;
95490
95689
  var AQ_TTL_MINUTES = 60;
95491
95690
  var MIN_MS = 60000;
95691
+ var ORPHAN_TTL_MS = 7 * 24 * 60 * MIN_MS;
95492
95692
  var conditionSchema = exports_external.enum([
95493
95693
  "clear",
95494
95694
  "mostly-clear",
@@ -95517,11 +95717,9 @@ var currentObsSchema = exports_external.object({
95517
95717
  windDirectionDeg: numberField,
95518
95718
  windGustKmh: nullableNumber,
95519
95719
  pressureHpa: nullableNumber,
95520
- cloudCoverPct: nullableNumber,
95521
95720
  dewPointC: nullableNumber,
95522
95721
  visibilityM: nullableNumber,
95523
95722
  uvIndex: nullableNumber,
95524
- precipLast1hMm: nullableNumber,
95525
95723
  isDay: exports_external.boolean()
95526
95724
  });
95527
95725
  var precipIntervalSchema = exports_external.object({
@@ -95552,11 +95750,9 @@ var dailyPointSchema = exports_external.object({
95552
95750
  tempMaxC: numberField,
95553
95751
  precipSumMm: numberField,
95554
95752
  precipProbabilityMaxPct: nullableNumber,
95555
- uvIndexMax: nullableNumber,
95556
95753
  sunriseUtc: exports_external.string().nullable(),
95557
95754
  sunsetUtc: exports_external.string().nullable(),
95558
- windSpeedMaxKmh: nullableNumber,
95559
- windGustMaxKmh: nullableNumber
95755
+ windSpeedMaxKmh: nullableNumber
95560
95756
  });
95561
95757
  var normalizedForecastSchema = exports_external.object({
95562
95758
  providerId: exports_external.string(),
@@ -95571,6 +95767,7 @@ var normalizedForecastSchema = exports_external.object({
95571
95767
  daily: exports_external.array(dailyPointSchema)
95572
95768
  });
95573
95769
  var envelopeSchema = exports_external.object({
95770
+ version: exports_external.literal(CACHE_SCHEMA_VERSION),
95574
95771
  fetchedAtUtc: exports_external.string().refine((s) => !Number.isNaN(Date.parse(s)), {
95575
95772
  message: "fetchedAtUtc is not a parseable instant"
95576
95773
  }),
@@ -95578,13 +95775,12 @@ var envelopeSchema = exports_external.object({
95578
95775
  });
95579
95776
  var airQualitySchema = exports_external.object({
95580
95777
  usAqi: exports_external.number().nullable(),
95581
- pm25UgM3: exports_external.number().nullable(),
95582
- ozoneUgM3: exports_external.number().nullable(),
95583
95778
  observedAtUtc: exports_external.string().refine((s) => !Number.isNaN(Date.parse(s)), {
95584
95779
  message: "observedAtUtc is not a parseable instant"
95585
95780
  })
95586
95781
  });
95587
95782
  var aqEnvelopeSchema = exports_external.object({
95783
+ version: exports_external.literal(CACHE_SCHEMA_VERSION),
95588
95784
  fetchedAtUtc: exports_external.string().refine((s) => !Number.isNaN(Date.parse(s)), {
95589
95785
  message: "fetchedAtUtc is not a parseable instant"
95590
95786
  }),
@@ -95592,11 +95788,11 @@ var aqEnvelopeSchema = exports_external.object({
95592
95788
  });
95593
95789
  function cacheKey(providerId, latitude, longitude, window2) {
95594
95790
  const windowTag = window2 === undefined ? "" : `|${window2.forecastDays ?? "*"}|${window2.forecastHours ?? "*"}`;
95595
- const digest = createHash("sha256").update(`${providerId}|${latitude.toFixed(3)}|${longitude.toFixed(3)}${windowTag}`).digest("hex");
95791
+ const digest = createHash("sha256").update(`${CACHE_SCHEMA_VERSION}|${providerId}|${latitude.toFixed(3)}|${longitude.toFixed(3)}${windowTag}`).digest("hex");
95596
95792
  return `${digest}.json`;
95597
95793
  }
95598
95794
  function airQualityCacheKey(providerId, latitude, longitude) {
95599
- const digest = createHash("sha256").update(`${providerId}|aq|${latitude.toFixed(3)}|${longitude.toFixed(3)}`).digest("hex");
95795
+ const digest = createHash("sha256").update(`${CACHE_SCHEMA_VERSION}|${providerId}|aq|${latitude.toFixed(3)}|${longitude.toFixed(3)}`).digest("hex");
95600
95796
  return `${digest}.json`;
95601
95797
  }
95602
95798
  function parseEnvelope(raw) {
@@ -95681,6 +95877,35 @@ class FsCacheIo {
95681
95877
  await rm(join7(await this.baseDir(), key), { force: true });
95682
95878
  }
95683
95879
  }
95880
+ async function sweepStaleCacheFiles(io, nowMs) {
95881
+ let dir;
95882
+ try {
95883
+ dir = await io.baseDir();
95884
+ } catch {
95885
+ return;
95886
+ }
95887
+ let entries;
95888
+ try {
95889
+ entries = await readdir(dir);
95890
+ } catch {
95891
+ return;
95892
+ }
95893
+ for (const entry of entries) {
95894
+ if (!entry.endsWith(".json"))
95895
+ continue;
95896
+ let mtimeMs;
95897
+ try {
95898
+ mtimeMs = (await stat2(join7(dir, entry))).mtimeMs;
95899
+ } catch {
95900
+ continue;
95901
+ }
95902
+ if (nowMs - mtimeMs > ORPHAN_TTL_MS) {
95903
+ await io.remove(entry).catch(() => {
95904
+ return;
95905
+ });
95906
+ }
95907
+ }
95908
+ }
95684
95909
  async function cachedForecast(provider, location, opts, io = new FsCacheIo) {
95685
95910
  const maxAgeMinutes = opts?.maxAgeMinutes ?? DEFAULT_MAX_AGE_MINUTES;
95686
95911
  const nowUtc = opts?.nowUtc ?? new Date().toISOString();
@@ -95698,7 +95923,14 @@ async function cachedForecast(provider, location, opts, io = new FsCacheIo) {
95698
95923
  }
95699
95924
  try {
95700
95925
  const forecast = await provider.getForecast(location, opts?.window);
95701
- await io.write(key, JSON.stringify({ fetchedAtUtc: nowUtc, forecast }));
95926
+ await io.write(key, JSON.stringify({
95927
+ version: CACHE_SCHEMA_VERSION,
95928
+ fetchedAtUtc: nowUtc,
95929
+ forecast
95930
+ }));
95931
+ await sweepStaleCacheFiles(io, nowMs).catch(() => {
95932
+ return;
95933
+ });
95702
95934
  return { forecast, stale: false };
95703
95935
  } catch (error61) {
95704
95936
  if (envelope && error61 instanceof ProviderError) {
@@ -95726,7 +95958,14 @@ async function cachedAirQuality(provider, location, opts, io = new FsCacheIo) {
95726
95958
  }
95727
95959
  try {
95728
95960
  const airQuality = await provider.getAirQuality(location);
95729
- await io.write(key, JSON.stringify({ fetchedAtUtc: nowUtc, airQuality }));
95961
+ await io.write(key, JSON.stringify({
95962
+ version: CACHE_SCHEMA_VERSION,
95963
+ fetchedAtUtc: nowUtc,
95964
+ airQuality
95965
+ }));
95966
+ await sweepStaleCacheFiles(io, nowMs).catch(() => {
95967
+ return;
95968
+ });
95730
95969
  return { airQuality, stale: false };
95731
95970
  } catch (error61) {
95732
95971
  if (envelope && error61 instanceof ProviderError) {
@@ -95763,8 +96002,14 @@ var DELETE_ARM_TTL_MS = 4000;
95763
96002
  function isDeleteArmed(armedAtMs, nowMs) {
95764
96003
  return armedAtMs !== null && nowMs >= armedAtMs && nowMs - armedAtMs < DELETE_ARM_TTL_MS;
95765
96004
  }
96005
+ var ACTION_ERROR_TTL_MS = DELETE_ARM_TTL_MS;
96006
+ function isActionErrorActive(atMs, nowMs) {
96007
+ return atMs !== null && nowMs >= atMs && nowMs - atMs < ACTION_ERROR_TTL_MS;
96008
+ }
95766
96009
  function errorMessage2(e) {
95767
- return e instanceof Error ? e.message : String(e);
96010
+ const raw = e instanceof Error ? e.message : String(e);
96011
+ return raw.split(`
96012
+ `)[0] ?? raw;
95768
96013
  }
95769
96014
  function findLocation(config2, slug) {
95770
96015
  return config2.locations.find((loc) => loc.slug === slug);
@@ -95784,6 +96029,21 @@ function resolveDefaultSlug(config2, explicitSlug) {
95784
96029
  }
95785
96030
  return config2.locations[0]?.slug ?? null;
95786
96031
  }
96032
+ function withRepairedDefault(config2) {
96033
+ if (config2.default_location === undefined)
96034
+ return config2;
96035
+ if (config2.locations.some((loc) => loc.slug === config2.default_location))
96036
+ return config2;
96037
+ if (config2.locations.length === 0) {
96038
+ const next = { ...config2 };
96039
+ delete next.default_location;
96040
+ return next;
96041
+ }
96042
+ const first = config2.locations[0];
96043
+ if (!first)
96044
+ return config2;
96045
+ return { ...config2, default_location: first.slug };
96046
+ }
95787
96047
  function createStoreInstance(deps = prodDeps()) {
95788
96048
  const fetcher = deps.fetchForecast;
95789
96049
  const aqFetcher = deps.fetchAirQuality;
@@ -95793,6 +96053,36 @@ function createStoreInstance(deps = prodDeps()) {
95793
96053
  let refreshHandle;
95794
96054
  let disposed = false;
95795
96055
  const inFlight = new Map;
96056
+ let actionErrorTimer;
96057
+ function clearActionErrorTimer() {
96058
+ if (actionErrorTimer !== undefined) {
96059
+ clearTimeout(actionErrorTimer);
96060
+ actionErrorTimer = undefined;
96061
+ }
96062
+ }
96063
+ function clearActionErrorState() {
96064
+ clearActionErrorTimer();
96065
+ const s = get();
96066
+ if (s.lastActionError !== undefined || s.lastActionErrorAtMs !== null) {
96067
+ set2({ lastActionError: undefined, lastActionErrorAtMs: null });
96068
+ }
96069
+ }
96070
+ function setActionErrorState(message) {
96071
+ clearActionErrorTimer();
96072
+ const now = Date.now();
96073
+ const flat = message.replace(/\s+/g, " ").trim();
96074
+ set2({ lastActionError: flat, lastActionErrorAtMs: now });
96075
+ actionErrorTimer = setTimeout(() => {
96076
+ if (disposed)
96077
+ return;
96078
+ const cur = get();
96079
+ if (cur.lastActionErrorAtMs === now) {
96080
+ set2({ lastActionError: undefined, lastActionErrorAtMs: null });
96081
+ }
96082
+ actionErrorTimer = undefined;
96083
+ }, ACTION_ERROR_TTL_MS);
96084
+ actionErrorTimer.unref?.();
96085
+ }
95796
96086
  function clearRefreshTimer() {
95797
96087
  if (refreshHandle === undefined)
95798
96088
  return;
@@ -95854,12 +96144,16 @@ function createStoreInstance(deps = prodDeps()) {
95854
96144
  airQuality: null,
95855
96145
  airQualityBySlug: {},
95856
96146
  lastActionError: undefined,
96147
+ lastActionErrorAtMs: null,
95857
96148
  helpOpen: false,
95858
96149
  overlayOpen: false,
95859
96150
  locationsOpen: false,
95860
96151
  deleteArmedAtMs: null,
96152
+ onboardingSkipped: false,
96153
+ onboardingForced: false,
95861
96154
  init: async (explicitSlug) => {
95862
- set2({ initStatus: "loading", lastActionError: undefined });
96155
+ clearActionErrorTimer();
96156
+ set2({ initStatus: "loading", lastActionError: undefined, lastActionErrorAtMs: null });
95863
96157
  try {
95864
96158
  const config2 = await loadConfig(deps.configPath);
95865
96159
  const slug = resolveDefaultSlug(config2, explicitSlug);
@@ -95872,7 +96166,8 @@ function createStoreInstance(deps = prodDeps()) {
95872
96166
  }
95873
96167
  scheduleRefreshLoop();
95874
96168
  } catch (e) {
95875
- set2({ initStatus: "error", lastActionError: errorMessage2(e) });
96169
+ clearActionErrorTimer();
96170
+ set2({ initStatus: "error", lastActionError: errorMessage2(e), lastActionErrorAtMs: null });
95876
96171
  }
95877
96172
  },
95878
96173
  loadForecast: async (slug, opts) => {
@@ -95928,12 +96223,14 @@ function createStoreInstance(deps = prodDeps()) {
95928
96223
  switchLocation: (slug) => {
95929
96224
  if (!findLocation(get().config, slug))
95930
96225
  return;
96226
+ clearActionErrorState();
95931
96227
  const aq = get().airQualityBySlug[slug] ?? null;
95932
96228
  set2({ activeSlug: slug, airQuality: aq });
95933
96229
  scheduleRefreshLoop();
95934
96230
  get().loadForecast(slug);
95935
96231
  },
95936
96232
  cycleLocation: (delta) => {
96233
+ clearActionErrorState();
95937
96234
  set2({ deleteArmedAtMs: null });
95938
96235
  const locations = get().config.locations;
95939
96236
  if (locations.length === 0)
@@ -95950,67 +96247,99 @@ function createStoreInstance(deps = prodDeps()) {
95950
96247
  const prefs = resolveDisplayPrefs(config2);
95951
96248
  const mixed = new Set([prefs.temp, prefs.wind, prefs.precip, prefs.pressure]).size > 1;
95952
96249
  const units = config2.units === "metric" ? "imperial" : "metric";
95953
- const next = mixed ? { ...config2, units, unit_prefs: { ...config2.unit_prefs, temp: units } } : {
96250
+ const rawNext = mixed ? { ...config2, units, unit_prefs: { ...config2.unit_prefs, temp: units } } : {
95954
96251
  ...config2,
95955
96252
  units,
95956
96253
  unit_prefs: { temp: units, wind: units, precip: units, pressure: units }
95957
96254
  };
96255
+ const next = withRepairedDefault(rawNext);
96256
+ clearActionErrorState();
95958
96257
  set2({ config: next });
95959
96258
  await saveConfig(next, deps.configPath).catch((e) => {
95960
- set2({ lastActionError: errorMessage2(e) });
96259
+ setActionErrorState(errorMessage2(e));
95961
96260
  });
95962
96261
  },
95963
- toggleHelp: () => set2((s) => ({ helpOpen: !s.helpOpen })),
95964
- setOverlayOpen: (open5) => set2(open5 ? { overlayOpen: true, locationsOpen: false, deleteArmedAtMs: null } : { overlayOpen: false }),
95965
- setLocationsOpen: (open5) => set2(open5 ? { locationsOpen: true, overlayOpen: false, deleteArmedAtMs: null } : { locationsOpen: false }),
95966
- armDelete: () => set2({ deleteArmedAtMs: Date.now() }),
95967
- disarmDelete: () => set2({ deleteArmedAtMs: null }),
96262
+ toggleHelp: () => {
96263
+ clearActionErrorState();
96264
+ set2((s) => ({ helpOpen: !s.helpOpen }));
96265
+ },
96266
+ setOverlayOpen: (open5) => {
96267
+ clearActionErrorState();
96268
+ set2(open5 ? { overlayOpen: true, locationsOpen: false, deleteArmedAtMs: null } : { overlayOpen: false });
96269
+ },
96270
+ setLocationsOpen: (open5) => {
96271
+ clearActionErrorState();
96272
+ set2(open5 ? { locationsOpen: true, overlayOpen: false, deleteArmedAtMs: null } : { locationsOpen: false });
96273
+ },
96274
+ armDelete: () => {
96275
+ clearActionErrorState();
96276
+ set2({ deleteArmedAtMs: Date.now() });
96277
+ },
96278
+ disarmDelete: () => {
96279
+ clearActionErrorState();
96280
+ set2({ deleteArmedAtMs: null });
96281
+ },
96282
+ clearActionError: () => {
96283
+ clearActionErrorState();
96284
+ },
95968
96285
  deleteArmed: (nowMs) => isDeleteArmed(get().deleteArmedAtMs, nowMs),
95969
96286
  searchLocations: (query) => geocoder(query),
95970
96287
  addLocation: async (entry) => {
96288
+ clearActionErrorState();
95971
96289
  const config2 = get().config;
95972
96290
  const slug = uniqueSlug(entry.slug, config2.locations.map((loc) => loc.slug));
95973
96291
  const finalEntry = slug === entry.slug ? entry : { ...entry, slug };
95974
96292
  const isFirstLocation = config2.locations.length === 0;
95975
- const next = {
96293
+ const rawNext = {
95976
96294
  ...config2,
95977
96295
  locations: [...config2.locations, finalEntry]
95978
96296
  };
95979
96297
  if (isFirstLocation && config2.default_location === undefined) {
95980
- next.default_location = slug;
96298
+ rawNext.default_location = slug;
95981
96299
  }
96300
+ const next = withRepairedDefault(rawNext);
95982
96301
  try {
95983
96302
  await saveConfig(next, deps.configPath);
95984
96303
  } catch (e) {
95985
- set2({ lastActionError: errorMessage2(e) });
95986
- return;
96304
+ setActionErrorState(errorMessage2(e));
96305
+ return false;
95987
96306
  }
95988
96307
  set2({ config: next });
95989
96308
  get().switchLocation(slug);
96309
+ clearActionErrorState();
96310
+ return true;
95990
96311
  },
95991
96312
  completeOnboarding: async (entry, units) => {
95992
96313
  const config2 = get().config;
95993
- if (config2.locations.length > 0) {
95994
- set2({ lastActionError: "onboarding is already complete" });
96314
+ const forced = get().onboardingForced;
96315
+ if (config2.locations.length > 0 && !forced) {
96316
+ setActionErrorState("onboarding is already complete");
95995
96317
  return false;
95996
96318
  }
95997
96319
  const slug = uniqueSlug(entry.slug, config2.locations.map((loc) => loc.slug));
95998
96320
  const finalEntry = slug === entry.slug ? entry : { ...entry, slug };
95999
- const next = {
96321
+ const rawNext = {
96000
96322
  ...config2,
96001
96323
  units,
96002
96324
  unit_prefs: { temp: units, wind: units, precip: units, pressure: units },
96003
96325
  default_location: slug,
96004
- locations: [finalEntry]
96326
+ locations: forced && config2.locations.length > 0 ? [...config2.locations, finalEntry] : [finalEntry]
96005
96327
  };
96006
- set2({ lastActionError: undefined });
96328
+ const next = withRepairedDefault(rawNext);
96329
+ clearActionErrorState();
96007
96330
  try {
96008
96331
  await saveConfig(next, deps.configPath);
96009
96332
  } catch (e) {
96010
- set2({ lastActionError: errorMessage2(e) });
96333
+ setActionErrorState(errorMessage2(e));
96011
96334
  return false;
96012
96335
  }
96013
- set2({ config: next, activeSlug: slug, lastActionError: undefined });
96336
+ clearActionErrorState();
96337
+ set2({
96338
+ config: next,
96339
+ activeSlug: slug,
96340
+ onboardingForced: false,
96341
+ onboardingSkipped: false
96342
+ });
96014
96343
  await get().loadForecast(slug);
96015
96344
  scheduleRefreshLoop();
96016
96345
  return true;
@@ -96026,31 +96355,34 @@ function createStoreInstance(deps = prodDeps()) {
96026
96355
  set2({ deleteArmedAtMs: null });
96027
96356
  const locations = config2.locations;
96028
96357
  if (locations.length <= 1) {
96029
- set2({ lastActionError: "cannot delete the only location" });
96358
+ setActionErrorState("cannot delete the only location");
96030
96359
  return;
96031
96360
  }
96361
+ clearActionErrorState();
96032
96362
  const idx = locations.findIndex((loc) => loc.slug === slug);
96033
96363
  if (idx === -1)
96034
96364
  return;
96035
96365
  const remaining = locations.filter((_2, i) => i !== idx);
96036
- const next = { ...config2, locations: remaining };
96366
+ const rawNext = { ...config2, locations: remaining };
96037
96367
  if (config2.default_location === slug) {
96038
96368
  const fallback = remaining[0];
96039
96369
  if (fallback) {
96040
- next.default_location = fallback.slug;
96370
+ rawNext.default_location = fallback.slug;
96041
96371
  } else {
96042
- delete next.default_location;
96372
+ delete rawNext.default_location;
96043
96373
  }
96044
96374
  }
96375
+ const next = withRepairedDefault(rawNext);
96045
96376
  const nextActive = remaining[Math.min(idx, remaining.length - 1)];
96046
96377
  set2((s) => ({
96047
96378
  config: next,
96048
96379
  airQualityBySlug: withoutKey(s.airQualityBySlug, slug)
96049
96380
  }));
96381
+ let saveError;
96050
96382
  try {
96051
96383
  await saveConfig(next, deps.configPath);
96052
96384
  } catch (e) {
96053
- set2({ lastActionError: errorMessage2(e) });
96385
+ saveError = errorMessage2(e);
96054
96386
  }
96055
96387
  if (slug === get().activeSlug) {
96056
96388
  if (nextActive) {
@@ -96060,19 +96392,23 @@ function createStoreInstance(deps = prodDeps()) {
96060
96392
  set2({ activeSlug: null, airQuality: null });
96061
96393
  }
96062
96394
  }
96395
+ if (saveError !== undefined)
96396
+ setActionErrorState(saveError);
96063
96397
  },
96064
96398
  setDefaultLocation: async (slug) => {
96065
96399
  const config2 = get().config;
96066
96400
  if (!findLocation(config2, slug))
96067
96401
  return;
96402
+ clearActionErrorState();
96068
96403
  const next = { ...config2, default_location: slug };
96069
96404
  try {
96070
96405
  await saveConfig(next, deps.configPath);
96071
96406
  } catch (e) {
96072
- set2({ lastActionError: errorMessage2(e) });
96407
+ setActionErrorState(errorMessage2(e));
96073
96408
  return;
96074
96409
  }
96075
96410
  set2({ config: next });
96411
+ clearActionErrorState();
96076
96412
  },
96077
96413
  moveLocation: async (slug, delta) => {
96078
96414
  const config2 = get().config;
@@ -96082,23 +96418,34 @@ function createStoreInstance(deps = prodDeps()) {
96082
96418
  const nextIdx = idx + delta;
96083
96419
  if (nextIdx < 0 || nextIdx >= config2.locations.length)
96084
96420
  return;
96421
+ clearActionErrorState();
96085
96422
  const nextLocations = [...config2.locations];
96086
96423
  const [moved] = nextLocations.splice(idx, 1);
96087
96424
  if (!moved)
96088
96425
  return;
96089
96426
  nextLocations.splice(nextIdx, 0, moved);
96090
- const next = { ...config2, locations: nextLocations };
96427
+ const next = withRepairedDefault({ ...config2, locations: nextLocations });
96091
96428
  try {
96092
96429
  await saveConfig(next, deps.configPath);
96093
96430
  } catch (e) {
96094
- set2({ lastActionError: errorMessage2(e) });
96431
+ setActionErrorState(errorMessage2(e));
96095
96432
  return;
96096
96433
  }
96097
96434
  set2({ config: next });
96435
+ clearActionErrorState();
96098
96436
  },
96437
+ skipOnboarding: () => set2({ onboardingSkipped: true, onboardingForced: false }),
96438
+ requestOnboarding: () => set2({
96439
+ onboardingSkipped: false,
96440
+ onboardingForced: true,
96441
+ helpOpen: false,
96442
+ overlayOpen: false,
96443
+ locationsOpen: false
96444
+ }),
96099
96445
  dispose: () => {
96100
96446
  disposed = true;
96101
96447
  clearRefreshTimer();
96448
+ clearActionErrorTimer();
96102
96449
  set2({ airQuality: null, airQualityBySlug: {} });
96103
96450
  inFlight.clear();
96104
96451
  }
@@ -96121,6 +96468,8 @@ function LocationsOverlay({ store, width, height }) {
96121
96468
  const config2 = store((s) => s.config);
96122
96469
  const activeSlug = store((s) => s.activeSlug);
96123
96470
  const forecastBySlug = store((s) => s.forecastBySlug);
96471
+ const lastActionError = store((s) => s.lastActionError);
96472
+ const lastActionErrorAtMs = store((s) => s.lastActionErrorAtMs);
96124
96473
  const [cursor, setCursor] = import_react27.useState(0);
96125
96474
  const [offset, setOffset] = import_react27.useState(0);
96126
96475
  const [armedSlug, setArmedSlug] = import_react27.useState(null);
@@ -96199,6 +96548,7 @@ function LocationsOverlay({ store, width, height }) {
96199
96548
  setCursor((c) => Math.max(0, Math.min(c, count - 2)));
96200
96549
  store.getState().deleteLocation(slug);
96201
96550
  } else {
96551
+ store.getState().clearActionError();
96202
96552
  setArmedSlug(slug);
96203
96553
  setArmedAtMs(Date.now());
96204
96554
  }
@@ -96230,6 +96580,7 @@ function LocationsOverlay({ store, width, height }) {
96230
96580
  const left = Math.max(0, Math.floor((width - boxWidth) / 2));
96231
96581
  const top = Math.max(0, Math.floor((height - boxHeight) / 2));
96232
96582
  const armedLabel = armed && armedSlug !== null ? locations.find((loc) => loc.slug === armedSlug)?.label ?? null : null;
96583
+ const overlayActionError = isActionErrorActive(lastActionErrorAtMs, Date.now()) ? lastActionError : undefined;
96233
96584
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
96234
96585
  position: "absolute",
96235
96586
  left,
@@ -96253,7 +96604,11 @@ function LocationsOverlay({ store, width, height }) {
96253
96604
  bg: palette.surface,
96254
96605
  children: "─".repeat(innerWidth)
96255
96606
  }, undefined, false, undefined, this),
96256
- armedLabel !== null ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96607
+ overlayActionError !== undefined ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96608
+ fg: palette.danger,
96609
+ bg: palette.surface,
96610
+ children: truncateCells(overlayActionError, Math.max(0, innerWidth - 1))
96611
+ }, undefined, false, undefined, this) : armedLabel !== null ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96257
96612
  fg: palette.danger,
96258
96613
  bg: palette.surface,
96259
96614
  children: truncateCells(`d again deletes ${armedLabel}`, innerWidth)
@@ -96470,6 +96825,10 @@ function FirstRun({ store, width, height, quit }) {
96470
96825
  return;
96471
96826
  }
96472
96827
  if (step === "welcome") {
96828
+ if (key.name === "s" && !key.ctrl && !key.meta && !key.option && !key.shift) {
96829
+ store.getState().skipOnboarding();
96830
+ return;
96831
+ }
96473
96832
  if (key.name === "return" || key.name === "enter" || key.name === "escape") {
96474
96833
  setStep("units");
96475
96834
  }
@@ -96567,7 +96926,7 @@ function FirstRun({ store, width, height, quit }) {
96567
96926
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96568
96927
  fg: palette.accent,
96569
96928
  bg: palette.surface,
96570
- children: truncateTo2("enter continue · esc skip tour · q quit", innerWidth)
96929
+ children: truncateTo2("enter/esc continue · s skip · q quit", innerWidth)
96571
96930
  }, undefined, false, undefined, this)
96572
96931
  ]
96573
96932
  }, undefined, true, undefined, this)
@@ -96614,27 +96973,6 @@ function FirstRun({ store, width, height, quit }) {
96614
96973
  }, undefined, false, undefined, this);
96615
96974
  }
96616
96975
 
96617
- // src/theme/detect.ts
96618
- var FALLBACK_APPEARANCE = { ink: "dark", background: null };
96619
- async function detectTerminalAppearance(query, timeoutMs = 300) {
96620
- let timer;
96621
- try {
96622
- const colors = await Promise.race([
96623
- query.getPalette({ timeout: timeoutMs }),
96624
- new Promise((_2, reject) => {
96625
- timer = setTimeout(() => reject(new Error("palette query timed out")), timeoutMs);
96626
- })
96627
- ]);
96628
- const background = colors?.defaultBackground ?? null;
96629
- return { ink: isDarkBackground(background) ? "dark" : "light", background };
96630
- } catch {
96631
- return FALLBACK_APPEARANCE;
96632
- } finally {
96633
- if (timer !== undefined)
96634
- clearTimeout(timer);
96635
- }
96636
- }
96637
-
96638
96976
  // src/viewport/useViewport.ts
96639
96977
  var import_react32 = __toESM(require_react(), 1);
96640
96978
 
@@ -96817,10 +97155,16 @@ var HELP_LINES = [
96817
97155
  { text: "l locations j/k focus enter open (lg)" },
96818
97156
  { text: "s default J/K reorder (lg) ↑↓ scroll" },
96819
97157
  { text: "/ search d delete (press twice)", dim: true },
97158
+ { text: "o re-run setup", dim: true },
96820
97159
  { text: "esc close / clear focus" }
96821
97160
  ];
96822
- function HelpOverlay({ width, height, providerLabel }) {
97161
+ function HelpOverlay({ store, width, height, providerLabel }) {
96823
97162
  const palette = usePalette();
97163
+ useKeyboard((key) => {
97164
+ if (key.name === "o" && !key.ctrl && !key.meta && !key.option && !key.shift) {
97165
+ store.getState().requestOnboarding();
97166
+ }
97167
+ });
96824
97168
  const boxWidth = Math.max(1, Math.min(HELP_BOX_WIDTH, width >= 32 ? width - 2 : width));
96825
97169
  const left = Math.max(0, Math.floor((width - boxWidth) / 2));
96826
97170
  const top = Math.max(0, Math.floor((height - HELP_BOX_HEIGHT) / 2));
@@ -96853,12 +97197,12 @@ function HelpOverlay({ width, height, providerLabel }) {
96853
97197
  }
96854
97198
 
96855
97199
  // src/app/components/Sidebar.tsx
96856
- var import_react33 = __toESM(require_react(), 1);
97200
+ var import_react34 = __toESM(require_react(), 1);
96857
97201
  var SIDEBAR_WIDTH = 26;
96858
97202
  function truncateTo3(text, width) {
96859
97203
  return truncateCells(text, width);
96860
97204
  }
96861
- var SidebarRow = import_react33.memo(function SidebarRow2({
97205
+ var SidebarRow = import_react34.memo(function SidebarRow2({
96862
97206
  slug,
96863
97207
  label,
96864
97208
  store,
@@ -96877,7 +97221,7 @@ var SidebarRow = import_react33.memo(function SidebarRow2({
96877
97221
  children: truncateTo3(`${bullet} ${truncateTo3(label, labelBudget)}${tail}`, SIDEBAR_WIDTH - 3)
96878
97222
  }, undefined, false, undefined, this);
96879
97223
  });
96880
- var Sidebar = import_react33.memo(function Sidebar2({ store, focusedSlug, prefs }) {
97224
+ var Sidebar = import_react34.memo(function Sidebar2({ store, focusedSlug, prefs }) {
96881
97225
  const palette = usePalette();
96882
97226
  const config2 = store((s) => s.config);
96883
97227
  const activeSlug = store((s) => s.activeSlug);
@@ -96899,17 +97243,23 @@ var Sidebar = import_react33.memo(function Sidebar2({ store, focusedSlug, prefs
96899
97243
  });
96900
97244
 
96901
97245
  // src/app/components/StatusArea.tsx
96902
- var import_react34 = __toESM(require_react(), 1);
97246
+ var import_react35 = __toESM(require_react(), 1);
96903
97247
  var SPINNER_FRAMES = ["|", "/", "-", "\\"];
96904
97248
  var SPINNER_INTERVAL_MS = 120;
96905
- function Spinner() {
96906
- const [frame, setFrame] = import_react34.useState(0);
96907
- import_react34.useEffect(() => {
97249
+ function Spinner({ reducedMotion }) {
97250
+ const [frame, setFrame] = import_react35.useState(0);
97251
+ import_react35.useEffect(() => {
97252
+ if (reducedMotion)
97253
+ return;
96908
97254
  const timer = setInterval(() => {
96909
97255
  setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
96910
97256
  }, SPINNER_INTERVAL_MS);
96911
97257
  return () => clearInterval(timer);
96912
- }, []);
97258
+ }, [reducedMotion]);
97259
+ if (reducedMotion)
97260
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
97261
+ children: "|"
97262
+ }, undefined, false, undefined, this);
96913
97263
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96914
97264
  children: SPINNER_FRAMES[frame] ?? "|"
96915
97265
  }, undefined, false, undefined, this);
@@ -96919,7 +97269,19 @@ function deleteArmLine(label, width) {
96919
97269
  const budget = width === undefined ? displayWidth(line) : Math.max(0, width - 1);
96920
97270
  return truncateCells(line, budget);
96921
97271
  }
96922
- function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
97272
+ function actionErrorLine(error61, width) {
97273
+ const budget = width === undefined ? displayWidth(error61) : Math.max(0, width - 1);
97274
+ return truncateCells(error61, budget);
97275
+ }
97276
+ function StatusArea({
97277
+ loading,
97278
+ error: error61,
97279
+ stale,
97280
+ deleteArm,
97281
+ actionError,
97282
+ width,
97283
+ reducedMotion
97284
+ }) {
96923
97285
  const palette = usePalette();
96924
97286
  if (deleteArm !== undefined) {
96925
97287
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
@@ -96930,12 +97292,23 @@ function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
96930
97292
  }, undefined, false, undefined, this)
96931
97293
  }, undefined, false, undefined, this);
96932
97294
  }
97295
+ if (actionError !== undefined) {
97296
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
97297
+ flexDirection: "row",
97298
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
97299
+ fg: palette.danger,
97300
+ children: actionErrorLine(actionError, width)
97301
+ }, undefined, false, undefined, this)
97302
+ }, undefined, false, undefined, this);
97303
+ }
96933
97304
  if (loading) {
96934
97305
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
96935
97306
  flexDirection: "row",
96936
97307
  gap: 1,
96937
97308
  children: [
96938
- /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Spinner, {}, undefined, false, undefined, this),
97309
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Spinner, {
97310
+ reducedMotion
97311
+ }, undefined, false, undefined, this),
96939
97312
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96940
97313
  fg: palette.accent,
96941
97314
  children: "syncing…"
@@ -96975,15 +97348,15 @@ function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
96975
97348
  }
96976
97349
 
96977
97350
  // src/app/hooks/useNowMs.ts
96978
- var import_react35 = __toESM(require_react(), 1);
97351
+ var import_react36 = __toESM(require_react(), 1);
96979
97352
 
96980
97353
  // src/app/tick.ts
96981
97354
  var TICK_INTERVAL_MS = 30000;
96982
97355
 
96983
97356
  // src/app/hooks/useNowMs.ts
96984
97357
  function useNowMs(propsNowMs) {
96985
- const [, setTick] = import_react35.useState(0);
96986
- import_react35.useEffect(() => {
97358
+ const [, setTick] = import_react36.useState(0);
97359
+ import_react36.useEffect(() => {
96987
97360
  if (propsNowMs !== undefined)
96988
97361
  return;
96989
97362
  const h2 = setInterval(() => setTick((v2) => v2 + 1), TICK_INTERVAL_MS);
@@ -97010,7 +97383,6 @@ function handleKey(nameOrEvent, api2) {
97010
97383
  api2.toggleHelp();
97011
97384
  return;
97012
97385
  }
97013
- api2.quit();
97014
97386
  return;
97015
97387
  }
97016
97388
  if (api2.searchOpen() || api2.locationsOpen() || api2.helpOpen())
@@ -97249,7 +97621,7 @@ function XsChips({
97249
97621
  children: truncateTo4(dailyChips(forecast.daily, prefs.temp), Math.max(0, width))
97250
97622
  }, undefined, false, undefined, this);
97251
97623
  }
97252
- var MainContent = import_react37.memo(function MainContent2({
97624
+ var MainContent = import_react38.memo(function MainContent2({
97253
97625
  tier,
97254
97626
  width,
97255
97627
  forecast,
@@ -97444,17 +97816,20 @@ function App(props = {}) {
97444
97816
  const locationsOpen = store((s) => s.locationsOpen);
97445
97817
  const airQuality = store((s) => s.airQuality);
97446
97818
  const lastActionError = store((s) => s.lastActionError);
97819
+ const lastActionErrorAtMs = store((s) => s.lastActionErrorAtMs);
97447
97820
  const deleteArmedAtMs = store((s) => s.deleteArmedAtMs);
97821
+ const onboardingSkipped = store((s) => s.onboardingSkipped);
97822
+ const onboardingForced = store((s) => s.onboardingForced);
97448
97823
  const viewport = useViewport();
97449
97824
  const renderer = useRenderer();
97450
97825
  const isDay = entry?.forecast.current.isDay ?? true;
97451
97826
  const appearance = props.appearance ?? FALLBACK_APPEARANCE;
97452
- const palette = import_react37.useMemo(() => buildPalette(config2.theme, isDay, appearance.ink, appearance.background), [config2.theme, isDay, appearance]);
97453
- const prefs = import_react37.useMemo(() => resolveDisplayPrefs(config2), [config2]);
97454
- import_react37.useEffect(() => {
97827
+ const palette = import_react38.useMemo(() => buildPalette(config2.theme, isDay, appearance.ink, appearance.background), [config2.theme, isDay, appearance]);
97828
+ const prefs = import_react38.useMemo(() => resolveDisplayPrefs(config2), [config2]);
97829
+ import_react38.useEffect(() => {
97455
97830
  store.getState().init(props.initialSlug);
97456
97831
  }, [store, props.initialSlug]);
97457
- import_react37.useEffect(() => {
97832
+ import_react38.useEffect(() => {
97458
97833
  return () => {
97459
97834
  store.getState().dispose();
97460
97835
  };
@@ -97466,17 +97841,18 @@ function App(props = {}) {
97466
97841
  const nowUtc = props.nowUtc ?? new Date(nowMs).toISOString();
97467
97842
  const tier = viewport.tier;
97468
97843
  const forecast = entry?.forecast;
97469
- const onboardingOpen = initStatus === "ready" && config2.locations.length === 0;
97844
+ const onboardingOpen = initStatus === "ready" && (config2.locations.length === 0 && !onboardingSkipped || onboardingForced);
97470
97845
  const deleteArmed = isDeleteArmed(deleteArmedAtMs, nowMs);
97471
- const [focusedSlug, setFocusedSlug] = import_react37.useState(null);
97472
- import_react37.useEffect(() => {
97846
+ const actionError = isActionErrorActive(lastActionErrorAtMs, Date.now()) ? lastActionError : undefined;
97847
+ const [focusedSlug, setFocusedSlug] = import_react38.useState(null);
97848
+ import_react38.useEffect(() => {
97473
97849
  if (tier !== "lg")
97474
97850
  setFocusedSlug(null);
97475
97851
  else if (focusedSlug !== null && !config2.locations.some((loc) => loc.slug === focusedSlug)) {
97476
97852
  setFocusedSlug(null);
97477
97853
  }
97478
97854
  }, [config2.locations, focusedSlug, tier]);
97479
- const api2 = import_react37.useMemo(() => ({
97855
+ const api2 = import_react38.useMemo(() => ({
97480
97856
  quit,
97481
97857
  activeSlug: () => store.getState().activeSlug,
97482
97858
  refresh: (slug) => void store.getState().refresh(slug),
@@ -97486,7 +97862,8 @@ function App(props = {}) {
97486
97862
  toggleHelp: () => store.getState().toggleHelp(),
97487
97863
  searchOpen: () => {
97488
97864
  const state = store.getState();
97489
- return state.overlayOpen || state.initStatus === "ready" && state.config.locations.length === 0;
97865
+ const onboardingActive = state.initStatus === "ready" && (state.config.locations.length === 0 && !state.onboardingSkipped || state.onboardingForced);
97866
+ return state.overlayOpen || onboardingActive;
97490
97867
  },
97491
97868
  openSearch: () => store.getState().setOverlayOpen(true),
97492
97869
  locationsOpen: () => store.getState().locationsOpen,
@@ -97529,7 +97906,9 @@ function App(props = {}) {
97529
97906
  error: error61,
97530
97907
  stale: staleBadge,
97531
97908
  deleteArm: deleteArmed ? { label } : undefined,
97532
- width: viewport.width
97909
+ actionError,
97910
+ width: tier === "lg" ? viewport.width - SIDEBAR_WIDTH : viewport.width,
97911
+ reducedMotion: config2.reduced_motion
97533
97912
  }, undefined, false, undefined, this);
97534
97913
  const footerColumnWidth = tier === "lg" ? viewport.width - SIDEBAR_WIDTH : viewport.width;
97535
97914
  const footer = /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Footer, {
@@ -97545,7 +97924,7 @@ function App(props = {}) {
97545
97924
  panels: config2.panels,
97546
97925
  nowUtc
97547
97926
  }) : null;
97548
- const statusRows = deleteArmed ? 1 : loading ? 1 : error61 !== undefined ? ERROR_PANEL_ROWS : staleBadge ? 1 : 0;
97927
+ const statusRows = deleteArmed ? 1 : actionError !== undefined ? 1 : loading ? 1 : error61 !== undefined ? ERROR_PANEL_ROWS : staleBadge ? 1 : 0;
97549
97928
  const statusBlock = statusRows > 0 ? statusRows + 1 : 0;
97550
97929
  const showOverflowHint = overflowEstimate !== null && viewport.height < overflowEstimate + MAIN_CHROME_ROWS + statusBlock;
97551
97930
  const mainScrollHeight = Math.max(1, viewport.height - MAIN_CHROME_ROWS - statusBlock - (showOverflowHint ? OVERFLOW_HINT_ROWS : 0));
@@ -97659,6 +98038,7 @@ function App(props = {}) {
97659
98038
  children: [
97660
98039
  body,
97661
98040
  helpOpen ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(HelpOverlay, {
98041
+ store,
97662
98042
  width: viewport.width,
97663
98043
  height: viewport.height,
97664
98044
  providerLabel: config2.provider === "nws" ? "api.weather.gov" : "open-meteo.com"
@@ -97679,6 +98059,34 @@ function App(props = {}) {
97679
98059
  }, undefined, false, undefined, this);
97680
98060
  }
97681
98061
 
98062
+ // src/app/AppearanceApp.tsx
98063
+ function AppearanceApp({ initialSlug, appearancePromise, store }) {
98064
+ const [appearance, setAppearance] = import_react40.useState(FALLBACK_APPEARANCE);
98065
+ const renderer = useRenderer();
98066
+ import_react40.useEffect(() => {
98067
+ let cancelled = false;
98068
+ appearancePromise.then((detected) => {
98069
+ if (cancelled)
98070
+ return;
98071
+ if (renderer.isDestroyed)
98072
+ return;
98073
+ try {
98074
+ setAppearance((prev) => appearancesEqual(prev, detected) ? prev : detected);
98075
+ } catch {}
98076
+ }).catch(() => {
98077
+ return;
98078
+ });
98079
+ return () => {
98080
+ cancelled = true;
98081
+ };
98082
+ }, [appearancePromise, renderer]);
98083
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(App, {
98084
+ store,
98085
+ initialSlug,
98086
+ appearance
98087
+ }, undefined, false, undefined, this);
98088
+ }
98089
+
97682
98090
  // src/app/oneline.ts
97683
98091
  var SEPARATOR = " · ";
97684
98092
  var ARROWS_8 = ["↑", "↗", "→", "↘", "↓", "↙", "←", "↖"];
@@ -97821,6 +98229,18 @@ function parseInterval(raw) {
97821
98229
  }
97822
98230
  return value;
97823
98231
  }
98232
+ function warnStaleDefault(config2, write) {
98233
+ if (config2.default_location !== undefined && config2.locations.length > 0 && !config2.locations.some((loc) => loc.slug === config2.default_location)) {
98234
+ const fallback = config2.locations[0]?.slug ?? "";
98235
+ let stale = "";
98236
+ for (const ch of config2.default_location) {
98237
+ const code = ch.codePointAt(0) ?? 0;
98238
+ if (code > 31 && code !== 127)
98239
+ stale += ch;
98240
+ }
98241
+ write(`warning: default_location "${stale}" does not match any [[locations]] slug — using "${fallback}"`);
98242
+ }
98243
+ }
97824
98244
  function parseArgs(argv) {
97825
98245
  if (argv.includes("--help") || argv.includes("-h")) {
97826
98246
  if (argv.length !== 1)
@@ -97910,6 +98330,22 @@ function parseArgs(argv) {
97910
98330
  return args;
97911
98331
  }
97912
98332
 
98333
+ // src/lib/runtime/ffi.ts
98334
+ function formatFfiUnavailableMessage(nodeVersion) {
98335
+ return `interactive TUI requires Bun or Node >= 26.4 (detected node ${nodeVersion}) — CLI-only flags (--version, --one-line) still work`;
98336
+ }
98337
+ async function probeFfiAvailable() {
98338
+ const bunVersion = process.versions.bun;
98339
+ if (bunVersion !== undefined && bunVersion !== "")
98340
+ return true;
98341
+ try {
98342
+ await import("node:ffi");
98343
+ return true;
98344
+ } catch {
98345
+ return false;
98346
+ }
98347
+ }
98348
+
97913
98349
  // src/index.tsx
97914
98350
  function stderr(message) {
97915
98351
  process.stderr.write(`${message}
@@ -97948,6 +98384,7 @@ function resolveLocationForCli(args, config2) {
97948
98384
  }
97949
98385
  async function runOneLine(args) {
97950
98386
  const config2 = await loadConfig();
98387
+ warnStaleDefault(config2, stderr);
97951
98388
  const resolved = resolveLocationForCli(args, config2);
97952
98389
  if ("error" in resolved) {
97953
98390
  const hint = config2.locations.length === 0 ? "; run tuiweather to set one up" : "";
@@ -97970,6 +98407,7 @@ async function runOneLine(args) {
97970
98407
  }
97971
98408
  async function runTui(locationArg) {
97972
98409
  const config2 = await loadConfig();
98410
+ warnStaleDefault(config2, stderr);
97973
98411
  let initialSlug;
97974
98412
  if (locationArg !== null || config2.locations.length > 0) {
97975
98413
  const resolved = resolveSlugFromConfig(config2, locationArg);
@@ -97980,17 +98418,25 @@ async function runTui(locationArg) {
97980
98418
  }
97981
98419
  initialSlug = resolved.slug;
97982
98420
  }
98421
+ if (!await probeFfiAvailable()) {
98422
+ stderr(formatFfiUnavailableMessage(process.versions.node));
98423
+ return 1;
98424
+ }
97983
98425
  const renderer = await createCliRenderer({ exitOnCtrlC: true });
97984
- const appearance = await detectTerminalAppearance(renderer);
97985
98426
  renderer.on("destroy", () => appStore.getState().dispose());
97986
- createRoot(renderer).render(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(App, {
98427
+ const appearancePromise = resolveTerminalAppearance(config2.ink, renderer);
98428
+ createRoot(renderer).render(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(AppearanceApp, {
97987
98429
  initialSlug,
97988
- appearance
98430
+ appearancePromise
97989
98431
  }, undefined, false, undefined, this));
98432
+ appearancePromise.catch(() => {
98433
+ return;
98434
+ });
97990
98435
  return 0;
97991
98436
  }
97992
98437
  async function runWatchCli(args) {
97993
98438
  const config2 = await loadConfig();
98439
+ warnStaleDefault(config2, stderr);
97994
98440
  const resolved = resolveLocationForCli(args, config2);
97995
98441
  if ("error" in resolved) {
97996
98442
  const hint = config2.locations.length === 0 ? "; run tuiweather to set one up" : "";