@indigoai-us/hq-cli 5.59.0 → 5.61.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.
@@ -8,7 +8,9 @@ import { CLI_VERSION } from "../cli-version.js";
8
8
  const PACKAGE_NAME = "@indigoai-us/hq-cli";
9
9
  const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
10
10
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
11
+ const CACHE_TTL_JITTER_MS = 60 * 60 * 1000;
11
12
  const FETCH_TIMEOUT_MS = 3_000;
13
+ const REFRESH_LOCK_STALE_MS = 10 * 60 * 1000;
12
14
 
13
15
  interface CacheEntry {
14
16
  latest: string;
@@ -19,6 +21,10 @@ function cachePath(): string {
19
21
  return path.join(os.homedir(), ".hq", "version-check.json");
20
22
  }
21
23
 
24
+ function lockPath(): string {
25
+ return path.join(os.homedir(), ".hq", "version-check.lock");
26
+ }
27
+
22
28
  function isOptedOut(): boolean {
23
29
  return process.env.HQ_NO_UPDATE_CHECK === "1";
24
30
  }
@@ -49,6 +55,58 @@ function writeCache(entry: CacheEntry): void {
49
55
  }
50
56
  }
51
57
 
58
+ function freshEnough(entry: CacheEntry, now = Date.now()): boolean {
59
+ const jitter = Math.floor(Math.random() * CACHE_TTL_JITTER_MS);
60
+ return now - entry.fetchedAt <= CACHE_TTL_MS - jitter;
61
+ }
62
+
63
+ function isKnownNoninteractiveStatusProbe(argv: readonly string[] = process.argv): boolean {
64
+ const args = argv.slice(2);
65
+ const positional = args.filter((arg) => !arg.startsWith("-"));
66
+ const json = args.includes("--json") || !process.stdout.isTTY;
67
+ if (!json) return false;
68
+ if (positional[0] === "mcp" && positional[1] === "status") return true;
69
+ if (positional[0] === "packs" && (positional[1] === "list" || positional[1] === "ls")) return true;
70
+ if (
71
+ positional[0] === "packages" &&
72
+ positional[1] === "packs" &&
73
+ (positional[2] === "list" || positional[2] === "ls")
74
+ ) {
75
+ return true;
76
+ }
77
+ if ((positional[0] === "sources" || positional[0] === "signals") && positional[1] === "list") {
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+
83
+ function acquireRefreshLock(now = Date.now()): (() => void) | null {
84
+ const dir = lockPath();
85
+ try {
86
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
87
+ fs.mkdirSync(dir);
88
+ fs.writeFileSync(path.join(dir, "owner"), `${process.pid}\n${now}\n`);
89
+ return () => {
90
+ try {
91
+ fs.rmSync(dir, { recursive: true, force: true });
92
+ } catch {
93
+ // best-effort lock cleanup
94
+ }
95
+ };
96
+ } catch {
97
+ try {
98
+ const stat = fs.statSync(dir);
99
+ if (now - stat.mtimeMs > REFRESH_LOCK_STALE_MS) {
100
+ fs.rmSync(dir, { recursive: true, force: true });
101
+ return acquireRefreshLock(now);
102
+ }
103
+ } catch {
104
+ // ignore lock inspection failures
105
+ }
106
+ return null;
107
+ }
108
+ }
109
+
52
110
  export function maybeWarnNewVersion(): void {
53
111
  if (isOptedOut()) return;
54
112
  const entry = readCache();
@@ -68,7 +126,14 @@ export function maybeWarnNewVersion(): void {
68
126
 
69
127
  export async function refreshVersionCache(): Promise<void> {
70
128
  if (isOptedOut()) return;
129
+ if (isKnownNoninteractiveStatusProbe()) return;
130
+ const existing = readCache();
131
+ if (existing && freshEnough(existing)) return;
132
+ const releaseLock = acquireRefreshLock();
133
+ if (!releaseLock) return;
71
134
  try {
135
+ const lockedExisting = readCache();
136
+ if (lockedExisting && freshEnough(lockedExisting)) return;
72
137
  const res = await fetch(REGISTRY_URL, {
73
138
  headers: { Accept: "application/json" },
74
139
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
@@ -79,5 +144,12 @@ export async function refreshVersionCache(): Promise<void> {
79
144
  writeCache({ latest: body.version, fetchedAt: Date.now() });
80
145
  } catch {
81
146
  // best-effort; offline / registry down / timeout — silent
147
+ } finally {
148
+ releaseLock();
82
149
  }
83
150
  }
151
+
152
+ export const __test__ = {
153
+ CACHE_TTL_MS,
154
+ isKnownNoninteractiveStatusProbe,
155
+ };