ticketlens 0.5.0 → 0.5.1

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.
@@ -31,12 +31,27 @@ import { readCliToken, saveCliToken, deleteCliToken } from '../skills/jtb/script
31
31
  import { browserLogin } from '../skills/jtb/scripts/lib/browser-login.mjs';
32
32
  import { syncProfiles, getApiBase, getConsoleBase } from '../skills/jtb/scripts/lib/sync.mjs';
33
33
  import { promptSecret, promptText } from '../skills/jtb/scripts/lib/prompt-helpers.mjs';
34
+ import { checkForUpdate, getUpdateHint } from '../skills/jtb/scripts/lib/update-check.mjs';
34
35
 
35
36
  const args = process.argv.slice(2);
36
37
  const { command, args: cmdArgs } = parseCommand(args);
37
38
 
38
39
  // Fire-and-forget: silently refresh license.json at startup if >7 days since last validation
39
40
  revalidateIfStale();
41
+ // Fire-and-forget: refresh the cached latest npm version once per 24h
42
+ checkForUpdate();
43
+
44
+ // Show a one-line update hint on stderr after the command exits (never blocks stdout)
45
+ process.on('exit', () => {
46
+ try {
47
+ const latest = getUpdateHint();
48
+ if (!latest) return;
49
+ const s = createStyler({ isTTY: process.stderr.isTTY });
50
+ process.stderr.write(`\n${s.brand('◆')} Update available ${s.dim('→')} ${s.bold(s.cyan(latest))} ${s.dim('npm install -g ticketlens')}\n`);
51
+ } catch {
52
+ // never let the update hint crash the process
53
+ }
54
+ });
40
55
 
41
56
  switch (command) {
42
57
  case 'fetch':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Jira CLI for developers — fetch ticket context, triage your queue, and stop tab-switching. Zero dependencies, all local.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Background npm update check.
3
+ * Caches the latest published version in ~/.ticketlens/update-check.json with a 24h TTL.
4
+ * Never blocks the CLI — all failures are silently swallowed.
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { DEFAULT_CONFIG_DIR, getVersion } from './config.mjs';
10
+
11
+ export const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
12
+ const REGISTRY_URL = 'https://registry.npmjs.org/ticketlens/latest';
13
+ const CHECK_FILE = 'update-check.json';
14
+
15
+ /** Path to the update-check cache file. */
16
+ function checkFilePath(configDir) {
17
+ return join(configDir, CHECK_FILE);
18
+ }
19
+
20
+ /**
21
+ * Returns the cached latestVersion string if the cache is present and within TTL,
22
+ * or null otherwise.
23
+ */
24
+ const SEMVER_RE = /^\d+\.\d+\.\d+$/;
25
+
26
+ export function readUpdateCheck(configDir = DEFAULT_CONFIG_DIR) {
27
+ try {
28
+ const data = JSON.parse(readFileSync(checkFilePath(configDir), 'utf8'));
29
+ if (!data?.checkedAt || typeof data.latestVersion !== 'string') return null;
30
+ if (!SEMVER_RE.test(data.latestVersion)) return null;
31
+ if (Date.now() - new Date(data.checkedAt).getTime() > UPDATE_CHECK_TTL_MS) return null;
32
+ return data.latestVersion;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Writes { checkedAt, latestVersion } to the cache file. Non-fatal.
40
+ */
41
+ export function writeUpdateCheck(configDir = DEFAULT_CONFIG_DIR, latestVersion) {
42
+ try {
43
+ mkdirSync(configDir, { recursive: true });
44
+ writeFileSync(
45
+ checkFilePath(configDir),
46
+ JSON.stringify({ checkedAt: new Date().toISOString(), latestVersion }),
47
+ { mode: 0o644 },
48
+ );
49
+ } catch {
50
+ // non-fatal
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Returns true when latest is strictly greater than current.
56
+ * Strips leading 'v' from both before comparing.
57
+ */
58
+ export function isNewer(latest, current) {
59
+ const parse = v => v.replace(/^v/, '').split('.').map(Number);
60
+ const [la, lb, lc] = parse(latest);
61
+ const [ca, cb, cc] = parse(current);
62
+ if (la !== ca) return la > ca;
63
+ if (lb !== cb) return lb > cb;
64
+ return lc > cc;
65
+ }
66
+
67
+ /**
68
+ * Fetches the latest version from the npm registry.
69
+ * Returns the version string, or null on any failure.
70
+ */
71
+ export async function fetchLatestVersion({ fetcher = globalThis.fetch } = {}) {
72
+ try {
73
+ const res = await fetcher(REGISTRY_URL, { signal: AbortSignal.timeout(10_000) });
74
+ if (!res.ok) return null;
75
+ const data = await res.json();
76
+ return data?.version ?? null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Fire-and-forget: checks if the cache is expired and, if so, fetches the latest
84
+ * version from npm and writes it to the cache. Errors are silently swallowed.
85
+ * Call at CLI startup; never await.
86
+ */
87
+ export function checkForUpdate({ configDir = DEFAULT_CONFIG_DIR, fetcher = globalThis.fetch } = {}) {
88
+ if (readUpdateCheck(configDir) !== null) return Promise.resolve();
89
+ return fetchLatestVersion({ fetcher })
90
+ .then(v => { if (v) writeUpdateCheck(configDir, v); })
91
+ .catch(() => {});
92
+ }
93
+
94
+ /**
95
+ * Returns the cached latestVersion string if it is newer than the current installed
96
+ * version, or null if no update is available or the cache is absent/expired.
97
+ */
98
+ export function getUpdateHint({ configDir = DEFAULT_CONFIG_DIR, currentVersion = getVersion() } = {}) {
99
+ const latest = readUpdateCheck(configDir);
100
+ if (!latest) return null;
101
+ return isNewer(latest, currentVersion) ? latest : null;
102
+ }