watchwhere 0.2.1 → 0.2.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchwhere",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "region-aware streaming availability CLI — find which of your subs has a given title",
5
5
  "type": "module",
6
6
  "bin": {
@@ -131,15 +131,15 @@ async function displayItem(
131
131
  const ads = regionData.ads ?? [];
132
132
  if (ads.length > 0) {
133
133
  console.log();
134
- console.log(` ${pad(m.ads, 6)}${joinNames(ads)}`);
134
+ console.log(` ${pad(m.ads, 10)}${joinNames(ads)}`);
135
135
  }
136
136
 
137
137
  const rent = regionData.rent ?? [];
138
138
  const buy = regionData.buy ?? [];
139
139
  if (rent.length > 0 || buy.length > 0) {
140
140
  if (ads.length === 0) console.log();
141
- if (rent.length > 0) console.log(` ${pad(m.rent, 6)}${joinNames(rent)}`);
142
- if (buy.length > 0) console.log(` ${pad(m.buy, 6)}${joinNames(buy)}`);
141
+ if (rent.length > 0) console.log(` ${pad(m.rent, 10)}${joinNames(rent)}`);
142
+ if (buy.length > 0) console.log(` ${pad(m.buy, 10)}${joinNames(buy)}`);
143
143
  }
144
144
 
145
145
  if (regionData.link) {
package/src/i18n.ts CHANGED
@@ -127,6 +127,9 @@ interface Catalog {
127
127
  // non-TTY
128
128
  ttyRequired: (cmd: string) => string;
129
129
  ambiguousQuery: (n: number) => string;
130
+
131
+ // update notifier
132
+ updateAvailable: (version: string) => string;
130
133
  }
131
134
 
132
135
  const en: Catalog = {
@@ -228,8 +231,10 @@ const en: Catalog = {
228
231
  relativeHours: (n) => `${n} hr ago`,
229
232
  relativeDays: (n) => `${n} day${n === 1 ? "" : "s"} ago`,
230
233
 
231
- ttyRequired: (cmd) => `\`ww ${cmd}\` is interactive run it in a terminal, not a pipe.`,
232
- ambiguousQuery: (n) => `${n} matches query is ambiguous. refine, or run interactively.`,
234
+ ttyRequired: (cmd) => `\`ww ${cmd}\` is interactive. run it in a terminal, not a pipe.`,
235
+ ambiguousQuery: (n) => `${n} matches, query is ambiguous. refine, or run interactively.`,
236
+
237
+ updateAvailable: (version) => `new version available: ${version} (bun install -g watchwhere)`,
233
238
  };
234
239
 
235
240
  const tr: Catalog = {
@@ -334,8 +339,10 @@ const tr: Catalog = {
334
339
  relativeHours: (n) => `${n} saat önce`,
335
340
  relativeDays: (n) => `${n} gün önce`,
336
341
 
337
- ttyRequired: (cmd) => `\`ww ${cmd}\` interaktif pipe'da değil, terminalde çalıştır.`,
338
- ambiguousQuery: (n) => `${n} eşleşme var sorgu belirsiz. daralt veya interaktif çalıştır.`,
342
+ ttyRequired: (cmd) => `\`ww ${cmd}\` interaktif. pipe'da değil, terminalde çalıştır.`,
343
+ ambiguousQuery: (n) => `${n} eşleşme var, sorgu belirsiz. daralt veya interaktif çalıştır.`,
344
+
345
+ updateAvailable: (version) => `yeni sürüm var: ${version} (bun install -g watchwhere)`,
339
346
  };
340
347
 
341
348
  const CATALOGS: Record<Locale, Catalog> = { en, tr };
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import { runSubs } from "./commands/subs.ts";
10
10
  import { runLang } from "./commands/lang.ts";
11
11
  import { runRegion } from "./commands/region.ts";
12
12
  import { TmdbError } from "./tmdb.ts";
13
+ import { checkForUpdate } from "./update-check.ts";
13
14
 
14
15
  function usage(m: ReturnType<typeof t>): string {
15
16
  return [
@@ -125,7 +126,31 @@ async function main(): Promise<void> {
125
126
  await runSearch(args.join(" "), cfg);
126
127
  }
127
128
 
128
- main().catch(async (err: unknown) => {
129
+ async function maybeNotifyUpdate(): Promise<void> {
130
+ // skip on quick lookups, only show after real work
131
+ const first = process.argv[2];
132
+ if (
133
+ !first ||
134
+ first === "--version" ||
135
+ first === "-v" ||
136
+ first === "--help" ||
137
+ first === "-h"
138
+ ) {
139
+ return;
140
+ }
141
+ try {
142
+ const newer = await checkForUpdate(pkg.version);
143
+ if (!newer) return;
144
+ const cfg = await loadConfig().catch(() => null);
145
+ const m = t(resolveLocale(cfg?.language));
146
+ console.log();
147
+ console.log(c.dim(` ${c.yellow("›")} ${m.updateAvailable(newer)}`));
148
+ } catch {
149
+ // best-effort, never block exit
150
+ }
151
+ }
152
+
153
+ main().then(maybeNotifyUpdate).catch(async (err: unknown) => {
129
154
  const cfg = await loadConfig().catch(() => null);
130
155
  const m = t(resolveLocale(cfg?.language));
131
156
 
@@ -0,0 +1,73 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ const CACHE_DIR = join(homedir(), ".watchwhere", "cache");
6
+ const CACHE_FILE = join(CACHE_DIR, "update.json");
7
+ const TTL_MS = 24 * 60 * 60 * 1000;
8
+ const REGISTRY_URL = "https://registry.npmjs.org/watchwhere/latest";
9
+ const FETCH_TIMEOUT_MS = 3000;
10
+
11
+ interface UpdateCache {
12
+ checkedAt: string;
13
+ latest: string;
14
+ }
15
+
16
+ function parseCache(raw: unknown): UpdateCache | null {
17
+ if (typeof raw !== "object" || raw === null) return null;
18
+ const v = raw as { checkedAt?: unknown; latest?: unknown };
19
+ if (typeof v.checkedAt !== "string" || typeof v.latest !== "string") return null;
20
+ return { checkedAt: v.checkedAt, latest: v.latest };
21
+ }
22
+
23
+ async function readCache(): Promise<UpdateCache | null> {
24
+ try {
25
+ const raw = await readFile(CACHE_FILE, "utf8");
26
+ const parsed = parseCache(JSON.parse(raw) as unknown);
27
+ if (!parsed) return null;
28
+ const age = Date.now() - Date.parse(parsed.checkedAt);
29
+ if (Number.isNaN(age) || age > TTL_MS || age < 0) return null;
30
+ return parsed;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ async function writeCache(latest: string): Promise<void> {
37
+ await mkdir(CACHE_DIR, { recursive: true, mode: 0o700 });
38
+ const entry: UpdateCache = { checkedAt: new Date().toISOString(), latest };
39
+ await writeFile(CACHE_FILE, JSON.stringify(entry), "utf8");
40
+ }
41
+
42
+ function isNewer(latest: string, current: string): boolean {
43
+ const a = latest.split(".").map((n) => Number.parseInt(n, 10));
44
+ const b = current.split(".").map((n) => Number.parseInt(n, 10));
45
+ for (let i = 0; i < 3; i++) {
46
+ const ai = a[i] ?? 0;
47
+ const bi = b[i] ?? 0;
48
+ if (Number.isNaN(ai) || Number.isNaN(bi)) return false;
49
+ if (ai > bi) return true;
50
+ if (ai < bi) return false;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ export async function checkForUpdate(currentVersion: string): Promise<string | null> {
56
+ try {
57
+ const cached = await readCache();
58
+ if (cached) {
59
+ return isNewer(cached.latest, currentVersion) ? cached.latest : null;
60
+ }
61
+ const res = await fetch(REGISTRY_URL, {
62
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
63
+ headers: { Accept: "application/json" },
64
+ });
65
+ if (!res.ok) return null;
66
+ const data = (await res.json()) as { version?: string };
67
+ if (typeof data.version !== "string") return null;
68
+ writeCache(data.version).catch(() => {});
69
+ return isNewer(data.version, currentVersion) ? data.version : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }