@aiwg/cli 2026.8.25 → 2026.8.27

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.
@@ -41,6 +41,7 @@ const CACHE_FILE = path.join(CACHE_DIR, 'update-notifier.json');
41
41
  * `lastCheckAt` timestamp in the cache file.
42
42
  */
43
43
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
44
+ const NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1000;
44
45
 
45
46
  /**
46
47
  * Hard timeout for the npm registry HTTPS request. Keeps the spawned
@@ -51,8 +52,8 @@ const NETWORK_TIMEOUT_MS = 10_000;
51
52
  /**
52
53
  * Read an env var and return true iff it's set to a truthy value.
53
54
  */
54
- function envFlag(name) {
55
- const v = process.env[name];
55
+ function envFlag(name, env = process.env) {
56
+ const v = env[name];
56
57
  if (!v) return false;
57
58
  return v !== '0' && v.toLowerCase() !== 'false';
58
59
  }
@@ -60,17 +61,21 @@ function envFlag(name) {
60
61
  /**
61
62
  * True iff update checking is currently disabled by env / CI conventions.
62
63
  */
63
- function isDisabled() {
64
- if (envFlag('NO_UPDATE_NOTIFIER')) return true;
65
- if (envFlag('AIWG_NO_UPDATE_CHECK')) return true;
66
- if (envFlag('CI')) return true;
67
- if (envFlag('GITHUB_ACTIONS')) return true;
68
- if (envFlag('GITLAB_CI')) return true;
64
+ export function notificationDisabled(env = process.env, isTTY = process.stdout.isTTY) {
65
+ if (envFlag('NO_UPDATE_NOTIFIER', env)) return true;
66
+ if (envFlag('AIWG_NO_UPDATE_CHECK', env)) return true;
67
+ if (envFlag('CI', env)) return true;
68
+ if (envFlag('GITHUB_ACTIONS', env)) return true;
69
+ if (envFlag('GITLAB_CI', env)) return true;
69
70
  // Non-interactive terminals shouldn't get a notice they can't act on.
70
- if (!process.stdout.isTTY) return true;
71
+ if (!isTTY) return true;
71
72
  return false;
72
73
  }
73
74
 
75
+ function isDisabled() {
76
+ return notificationDisabled();
77
+ }
78
+
74
79
  /**
75
80
  * Read the update-notifier cache. Returns null if absent or malformed.
76
81
  */
@@ -168,6 +173,35 @@ export function cacheMatchesPackage(cache, packageRoot) {
168
173
  return !!(currentVersion && cache?.current === currentVersion);
169
174
  }
170
175
 
176
+ export function formatUpdateNotice(current, latest) {
177
+ return (
178
+ `aiwg: update available ${current} → ${latest}\n` +
179
+ 'Update: aiwg update\n' +
180
+ 'Then rerun your command.\n'
181
+ );
182
+ }
183
+
184
+ export function noticeIsRateLimited(cache, intervalMs, now = Date.now()) {
185
+ if (!cache?.lastNotifiedAt) return false;
186
+ const age = now - new Date(cache.lastNotifiedAt).getTime();
187
+ return Number.isFinite(age) && age >= 0 && age < intervalMs;
188
+ }
189
+
190
+ /** Resolve the package that the canonical installation record says is active. */
191
+ export function resolveActivePackageRoot(launcherPackageRoot) {
192
+ try {
193
+ const installation = loadInstallationIdentity({
194
+ actualRoot: launcherPackageRoot,
195
+ createIfMissing: false,
196
+ });
197
+ return installation?.root ?? launcherPackageRoot;
198
+ } catch {
199
+ // A broken installation identity is diagnosed by the installation
200
+ // preflight. The best-effort notifier must never mask that recovery path.
201
+ return launcherPackageRoot;
202
+ }
203
+ }
204
+
171
205
  /**
172
206
  * Run the actual update check and write the cache file. Invoked by the
173
207
  * spawned background child via `node src/update/notifier.mjs --check`.
@@ -192,7 +226,12 @@ async function runCheck(packageRoot) {
192
226
  */
193
227
  export function scheduleBackgroundCheck(packageRoot) {
194
228
  if (isDisabled()) return;
195
- const installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
229
+ let installation;
230
+ try {
231
+ installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
232
+ } catch {
233
+ return;
234
+ }
196
235
  if (installation?.checkOnStartup === false) return;
197
236
 
198
237
  const cache = readCache();
@@ -228,14 +267,20 @@ export function scheduleBackgroundCheck(packageRoot) {
228
267
  */
229
268
  export function maybePrintNotice(packageRoot) {
230
269
  if (isDisabled()) return;
231
- const installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
270
+ let installation;
271
+ try {
272
+ installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
273
+ } catch {
274
+ return;
275
+ }
232
276
  if (installation?.checkOnStartup === false) return;
233
277
  const cache = readCache();
234
278
  if (!cacheMatchesPackage(cache, packageRoot)) return;
235
279
  if (!cache?.hasUpdate || !cache.current || !cache.latest) return;
236
- // One-line, non-intrusive. Users who want to update run `aiwg update`.
237
- const msg = `aiwg: update available ${cache.current} ${cache.latest} (run: aiwg update)`;
238
- process.stderr.write(`\n${msg}\n`);
280
+ const noticeInterval = installation?.updateCheckInterval ?? NOTICE_INTERVAL_MS;
281
+ if (noticeIsRateLimited(cache, noticeInterval)) return;
282
+ process.stderr.write(formatUpdateNotice(cache.current, cache.latest));
283
+ writeCache({ ...cache, lastNotifiedAt: new Date().toISOString() });
239
284
  }
240
285
 
241
286
  // ── Child-process entry ────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.25",
3
+ "version": "2026.8.27",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",