Skip to content

feat(cli): add internal update notifier package - #18502

Merged
rstoenescu merged 3 commits into
quasarframework:devfrom
hawkeye64:agent/publishable-update-notifier
Aug 5, 2026
Merged

rstoenescu merged 3 commits into
quasarframework:devfrom
hawkeye64:agent/publishable-update-notifier

Conversation

@hawkeye64

@hawkeye64 hawkeye64 commented Aug 4, 2026 •

Copy link
Copy Markdown
Member

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Documentation
  • Code style update
  • Refactor
  • Build-related changes
  • Other, please describe:

Does this PR introduce a breaking change?

  • Yes
  • No

The PR fulfills these requirements:

  • It is submitted to the dev branch (or v[X] branch)
  • When resolving a specific issue, it is referenced in the PR title
  • It has been tested on a Cordova (iOS, Android) app
  • It has been tested on an Electron app
  • Any necessary documentation has been added or updated in the docs or explained in the PR description.

If adding a new feature, the PR description includes:

Other information:

This addresses the dependency concern raised in discussion #18497 without introducing duplicated source or relying on a third-party replacement.

The new @quasar/update-notifier package follows the same model as Quasar internal packages such as @quasar/art and @quasar/ssl-certificate: it is independently publishable from the monorepo but identified as used internally by Quasar CLI(s). It is not intended to require a public community release announcement.

The package has no runtime dependencies and exposes one public function, notifyUpdate. @quasar/cli, create-quasar, and @quasar/icongenie all consume that single implementation. The notifier provides daily cached checks, detached registry requests that do not delay commands, one-shot terminal notifications, SemVer comparison, custom registry support, and opt-outs for CI, npm scripts, tests, and explicit environment or CLI flags.

Publishing order: @quasar/update-notifier@1.0.0 must be published before releasing updated CLI consumers. IconGenie remains outside the root pnpm workspace and therefore declares the published ^1.0.0 dependency directly.

This removes update-notifier and its transitive dependency chain from the workspace lockfile. There are no SSR, hydration, platform, accessibility, or security contract changes. No quasar.dev documentation is needed because this remains internal CLI behavior; the package README labels it accordingly.

Validation:

  • pnpm lint
  • pnpm --filter @quasar/update-notifier test (5 tests passed)
  • pnpm test (260 UI files / 5,156 tests plus all Vite plugin usage and runtime suites passed)
  • isolated npm installation of packed @quasar/update-notifier, @quasar/cli, create-quasar, and @quasar/icongenie artifacts
  • packed CLI smoke tests: quasar 5.0.4, create-quasar 5.0.9, and icongenie 6.1.2
  • verified the published export surface contains only notifyUpdate
  • git diff --check

Summary by CodeRabbit

  • New Features

    • Added update notifications across Quasar command-line tools.
    • Notifications detect newer versions and display them when appropriate.
    • Added cached and background update checks with configurable registry support.
  • Documentation

    • Added documentation covering usage, support, contributions, licensing, and versioning.
  • Tests

    • Added coverage for version comparisons, notification rendering, registry checks, caching, and notification conditions.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b3202ae1-8ac8-43ea-b0e8-70ad286f8b45

📥 Commits

Reviewing files that changed from the base of the PR and between 71aea5b and fe77b63.

📒 Files selected for processing (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

📝 Walkthrough

Walkthrough

Added @quasar/update-notifier with SemVer checks, cache handling, registry checks, background checks, notifications, and tests. Migrated Quasar CLI, create-quasar, and IconGenie to the shared package.

Changes

Update notifier migration

Layer / File(s) Summary
Notifier implementation and validation
utils/update-notifier/...
Added the package manifest, public export, notification logic, SemVer comparison, cache management, registry checks, background checks, documentation, and tests.
CLI integration and dependency wiring
pnpm-workspace.yaml, cli/..., create-quasar/..., icongenie/..., package.json
Added the package to the workspace, replaced update-notifier dependencies, migrated three CLI entry points to notifyUpdate, and added the package test command.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant notifyUpdate
  participant Cache
  participant NpmRegistry
  CLI->>notifyUpdate: pass package metadata
  notifyUpdate->>Cache: read cached update
  notifyUpdate->>CLI: display cached notification
  notifyUpdate->>NpmRegistry: check latest version in background
  NpmRegistry-->>Cache: store latest version
Loading

Suggested labels: area/cli, area/project-creation, area/icongenie, area/utils

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required Conventional Commit format and accurately describes adding the internal update notifier package for the CLI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Undocumented Breaking Change ✅ Passed The diff changes only internal notifier wiring and dependencies; CLI commands, flags, and configuration keys remain unchanged, and the description explicitly marks the PR as non-breaking.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hawkeye64
hawkeye64 force-pushed the agent/publishable-update-notifier branch from 7b360c9 to cef3ebd Compare August 4, 2026 18:13
@hawkeye64
hawkeye64 marked this pull request as ready for review August 4, 2026 18:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@utils/update-notifier/README.md`:
- Line 7: Update the npm version badge img element in the README to include a
concise alt attribute describing the badge, while preserving its existing source
and display.

In `@utils/update-notifier/src/internal.js`:
- Around line 158-160: The missing-cache branch in the update-check flow should
seed the cache and launch the detached background update check before returning,
rather than returning immediately after writeCache. Update the surrounding
internal.js logic and add coverage for the missing cache file case, verifying
the initial check starts immediately.
- Around line 163-178: Move the writeCache call from the unconditional
latest-value block into the notification branch after the process.once('exit',
...) handler is registered. Keep the cache update limited to cases where
isNewerVersion(latest, version) passes and the notification is displayed,
preserving cache.latest otherwise.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8eefb991-ace4-43e0-9fb8-250908a6f6dc

📥 Commits

Reviewing files that changed from the base of the PR and between 5f2fdd0 and cef3ebd.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (13)
  • cli/bin/quasar.js
  • cli/package.json
  • create-quasar/bin/create-quasar.js
  • create-quasar/package.json
  • icongenie/bin/icongenie.js
  • icongenie/package.json
  • package.json
  • pnpm-workspace.yaml
  • utils/update-notifier/README.md
  • utils/update-notifier/package.json
  • utils/update-notifier/src/index.js
  • utils/update-notifier/src/internal.js
  • utils/update-notifier/test/index.test.js

Comment thread utils/update-notifier/README.md Outdated
Comment thread utils/update-notifier/src/internal.js
Comment thread utils/update-notifier/src/internal.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@utils/update-notifier/src/internal.js`:
- Around line 152-156: Update the child process creation in the spawn call to
attach an error listener before invoking unref(), ensuring startup failures are
handled without crashing the CLI. Preserve the existing detached, ignored-stdio
behavior and command arguments.
- Around line 152-156: Update the spawn options in the background update-check
invocation to include windowsHide: true alongside detached and stdio, ensuring
the detached Node process remains invisible on Windows.
- Around line 187-189: Update the cache handling used by checkForUpdate and
getCacheFile so cached update results are partitioned by npm_config_registry.
Include the registry identity in the cache key or store and validate it with the
cached package data, ignoring stale cache entries when the active registry
changes while preserving existing behavior for the same registry.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: efe9b005-09f1-4e0d-a1f0-a8926a369021

📥 Commits

Reviewing files that changed from the base of the PR and between cef3ebd and 71aea5b.

📒 Files selected for processing (3)
  • utils/update-notifier/README.md
  • utils/update-notifier/src/internal.js
  • utils/update-notifier/test/index.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • utils/update-notifier/README.md

Comment on lines +152 to +156
spawn(
process.execPath,
[fileURLToPath(moduleUrl), backgroundCheckFlag, cacheFile, name, version],
{ detached: true, stdio: 'ignore' }
).unref()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '151,165p' utils/update-notifier/src/internal.js

Repository: quasarframework/quasar

Length of output: 584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant imports and functions:"
sed -n '1,80p' utils/update-notifier/src/internal.js
sed -n '140,210p' utils/update-notifier/src/internal.js

echo
echo "Search for startBackgroundCheck callers/registrations:"
rg -n "startBackgroundCheck|notifyUpdate|update_notifier|update-notifier" -S .

echo
echo "Node ChildProcess spawn error behavior probe:"
node - <<'JS'
const { spawn } = require('node:child_process')
const events = []
try {
  const child = spawn('this-binary-must-not-exist', ['--arg'], { detached: true, stdio: 'ignore' })
  child.once('error', (err) => events.push({ event: 'error', name: err.name, code: err.code }))
  child.unref()
  setTimeout(() => {
    console.log(JSON.stringify(events))
  }, 100)
} catch (error) {
  console.log(JSON.stringify({ event: 'throw', name: error.name, code: error.code }))
}
JS

Repository: quasarframework/quasar

Length of output: 7771


Handle spawned child-process errors.

spawn() emits error if the child cannot start. In this path, callers can exit before attaching the needed listener, so an update-check spawn failure can crash the CLI. Add a child-process error listener before unref().

Proposed fix
 function startBackgroundCheck(cacheFile, name, version) {
-  spawn(
+  const child = spawn(
     process.execPath,
     [fileURLToPath(moduleUrl), backgroundCheckFlag, cacheFile, name, version],
     { detached: true, stdio: 'ignore' }
-  ).unref()
+  )
+  child.on('error', () => undefined)
+  child.unref()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
spawn(
process.execPath,
[fileURLToPath(moduleUrl), backgroundCheckFlag, cacheFile, name, version],
{ detached: true, stdio: 'ignore' }
).unref()
const child = spawn(
process.execPath,
[fileURLToPath(moduleUrl), backgroundCheckFlag, cacheFile, name, version],
{ detached: true, stdio: 'ignore' }
)
child.on('error', () => undefined)
child.unref()
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/update-notifier/src/internal.js` around lines 152 - 156, Update the
child process creation in the spawn call to attach an error listener before
invoking unref(), ensuring startup failures are handled without crashing the
CLI. Preserve the existing detached, ignored-stdio behavior and command
arguments.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '151,157p' utils/update-notifier/src/internal.js

Repository: quasarframework/quasar

Length of output: 382


🌐 Web query:

Node.js child_process spawn detached Windows creates new console windowsHide true stdio ignore

💡 Result:

When using child_process.spawn on Windows with { detached: true }, Node.js by default creates a new console window for the child process [1][2]. To manage this behavior, consider the following technical details and workarounds: 1. The windowsHide Option The { windowsHide: true } option is intended to hide the subprocess console window [3][4]. However, in some scenarios—particularly when the spawned process is a shell script (.bat or.cmd files) or when combined with certain detached configurations—this option may not effectively suppress the window [5][6]. 2. Handling Standard I/O If you use { detached: true }, the subprocess will not remain running in the background after the parent process exits unless you also configure its stdio correctly [1][7]. Setting { stdio: 'ignore' } is standard practice for detached processes to ensure they are not connected to the parent's terminal, preventing the child from inheriting the parent's console and potentially keeping it alive [1][7]. 3. Common Workarounds If { windowsHide: true } does not hide the window as expected, developers often use the following techniques: - Use Direct Executables: Instead of spawning a.bat or.cmd file (which inherently requires a shell/terminal window), spawn the underlying executable directly (e.g., node.exe for a.js file) [5][6]. - Use VBScript/Powershell: A common workaround to run batch files invisibly is to invoke them via a VBScript wrapper that uses WScript.Shell with a hidden window flag, or using PowerShell's Start-Process with the -WindowStyle Hidden argument [6]. - Unref: Remember to call subprocess.unref if you want the parent process to be able to exit independently of the child [8][3]. In summary, while { detached: true, stdio: 'ignore', windowsHide: true } is the documented approach for attempting to run a background process, the effectiveness of windowsHide can be inconsistent depending on the target command [5][6]. Moving away from shell scripts to direct binary execution is the most reliable way to avoid console window creation [5].

Citations:


Hide the Windows console for the background update check.

spawn() with { detached: true } can open a child console on Windows; add windowsHide: true to keep the background Node process invisible.

Proposed fix
-    { detached: true, stdio: 'ignore' }
+    { detached: true, stdio: 'ignore', windowsHide: true }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
spawn(
process.execPath,
[fileURLToPath(moduleUrl), backgroundCheckFlag, cacheFile, name, version],
{ detached: true, stdio: 'ignore' }
).unref()
spawn(
process.execPath,
[fileURLToPath(moduleUrl), backgroundCheckFlag, cacheFile, name, version],
{ detached: true, stdio: 'ignore', windowsHide: true }
).unref()
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/update-notifier/src/internal.js` around lines 152 - 156, Update the
spawn options in the background update-check invocation to include windowsHide:
true alongside detached and stdio, ensuring the detached Node process remains
invisible on Windows.

Comment on lines +187 to +189
if (now - cache.checkedAt < checkInterval) return

startBackgroundCheck(cacheFile, name, version)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ast-grep outline utils/update-notifier/src/internal.js --items all --type function
rg -n -C 5 'getCacheFile|npm_config_registry|registry|checkedAt|latest' \
  utils/update-notifier/src/internal.js \
  utils/update-notifier/test/index.test.js

Repository: quasarframework/quasar

Length of output: 18862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- internal.js relevant lines ---'
cat -n utils/update-notifier/src/internal.js | sed -n '1,210p'

echo '--- package-specific tests and registry-related occurrences ---'
grep -RIn 'npm_config_registry|registry|dist-tags|latest|getCacheFile|cacheFile' utils/update-notifier -n

echo '--- deterministic cache path and interval behavior probe ---'
node - <<'JS'
import { readFileSync } from 'node:fs';
import vm from 'node:vm';

const code = readFileSync('utils/update-notifier/src/internal.js', 'utf8');

const sandbox = {
  console,
  process: {
    argv: ['node', 'internal.js'],
    once() {},
    stdout: { isTTY: true },
    env: { npm_config_registry: 'https://registry.npmjs.org' },
  },
};
const moduleExports = {};
const fn = vm.compileFunction(code, ['exports'], { filename: 'internal.js' });
fn(moduleExports);

const { getCacheFile, notifyUpdate, startBackgroundCheck, checkForUpdate, isNewerVersion } = moduleExports;

console.log('getCacheFile quasar CLI:', getCacheFile('`@quasar/cli`'));

const base = '/tmp/cache';
const path1 = getCacheFile('`@quasar/cli`');
const base2 = getCacheFile('`@quasar/cli`');
console.log('same getCacheFile path for same package:', path1 === base2);
console.log('same getCacheFile path for different package:', path1 === getCacheFile('`@quasar/other-cli`'));

let requestedUrl, writtenCache, startTime;
vm.modifyGlobal = function() {};
function setupDetachedProbe() {
  const src = code.replace('const backgroundCheckFlag =', 'console.error("started-background-check"); const backgroundCheckFlag =');
  const moduleExports2 = {};
  const fn2 = vm.compileFunction(src, ['exports'], { filename: 'internal.js' });
  fn2(moduleExports2);
  globals = moduleExports2;
  return globals;
}

let requestedUrl2, writtenCache2;
function setupFetchAndSpawnMocks(g) {
  global.fetch = async (url) => Object.assign({}, {
    ok: true,
    json: async () => ({ latest: '9.9.9' }),
  }, { url: url.href });
  g.spawn = (cmd, args, opts) => {
    requestedUrl2 = url;
    return {
      on(event, cb) {},
      unref() {},
    };
  };
  g.writeFile = async (cacheFile, data) => { writtenCache2 = JSON.parse(data); };
  g.readFile = async (cacheFile) => JSON.stringify({ checkedAt: Date.now(), latest: '1.1.1' });
  g.join = (a,b) => `${a}/${b}`;
  g.mkdir = async () => {};
}
const g = setupDetachedProbe();
setupFetchAndSpawnMocks(g);
console.log(g.checkForUpdate({ cacheFile: '/tmp/quasar.json', name: '`@quasar/cli`', version: '1.0.0' }).then(() => JSON.stringify({ requestedUrl2, requestedUrl2: requestedUrl2, writtenCache2, requestedUrl, writtenCache })));
JS

echo '--- no external dependencies in update-notifier js ---'
node - <<'JS'
const fs = require('fs');
const code = fs.readFileSync('utils/update-notifier/src/internal.js','utf8').split('\n').slice(0,22).join('\n');
console.log(code);
console.log('uses fetch:', code.includes('fetch'));
console.log('uses spawn:', code.includes('spawn'));
console.log('uses writeFile:', code.includes('writeFile'));
JS

Repository: quasarframework/quasar

Length of output: 7182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package-specific tests and registry-related occurrences ---'
rg -n -C 3 '-registry|registry|dist-tags|npm_config_registry|cacheFile|getCacheFile|startBackgroundCheck|notifyUpdate|BackgroundCheckFlag|checkForUpdate' utils/update-notifier

echo '--- package-specific cache path behavior ---'
node - <<'JS'
const path = require('path');
const os = require('os');

function getCacheFile(packageName) {
  const cacheDir =
    process.env.XDG_CACHE_HOME ||
    process.env.LOCALAPPDATA ||
    path.join(os.homedir(), '.cache')
  return path.join(cacheDir, 'quasar', 'update-notifier', `${encodeURIComponent(packageName)}.json`)
}

console.log('getCacheFile `@quasar/cli`:', getCacheFile('`@quasar/cli`'));
console.log('getCacheFile `@quasar/other`:', getCacheFile('`@quasar/other`'));
console.log('getCacheFile quasar/cli:', getCacheFile('quasar/cli'));
console.log('normalizedRegistry from URL pathname:', JSON.stringify([
  { input: 'https://registry.npmjs.org', pathname: new URL('https://registry.npmjs.org').pathname },
  { input: 'https://registry.npmjs.org/', pathname: new URL('https://registry.npmjs.org/').pathname },
]));
JS

echo '--- deterministic registry reuse scenario ---'
cat > /tmp/update-notifier-reg-scoped-probe.mjs <<'JS'
import { pathToFileURL } from 'node:url';
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const cacheRoot = join(tmpdir(), `quasar-update-notifier-registry-probe-${process.pid}`);
let requestedUrls = [];
let writtenCaches = [];

// Inline stable implementation from utils/update-notifier/src/internal.js without module dependencies.
function dirname(p) { return p.slice(0, Math.max(0, p.lastIndexOf('/'))) }
function getCacheFile(packageName) {
  return join(cacheRoot, 'update-notifier', `${encodeURIComponent(packageName)}.json`)
}
function readCache(cacheFile) {
  try {
    return JSON.parse(readFileSync(cacheFile, 'utf8'))
  } catch {
    return void 0
  }
}
function writeCache(cacheFile, value) {
  mkdirSync(dirname(cacheFile), { recursive: true })
  writeFileSync(cacheFile, JSON.stringify(value))
}
function parseVersion(version) {
  if (typeof version !== 'string') return void 0
  const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?(?:\+[\dA-Za-z-]+)?)$/i)
  if (!match) return void 0
  return {
    core: match.slice(1, 4),
    prerelease: match[4]?.split('.')
  }
}
function compareNumeric(left, right) {
  if (left.length !== right.length) return left.length > right.length
  return left > right
}
function isNewerVersion(next, current) {
  const installed = parseVersion(current)
  const nextParsed = parseVersion(next)
  if (!nextParsed || !installed) return false
  for (let index = 0; index < 3; index++) {
    if (nextParsed.core[index] !== installed.core[index]) return compareNumeric(nextParsed.core[index], installed.core[index])
  }
  if (!nextParsed.prerelease) return installed.prerelease !== void 0
  if (!installed.prerelease) return false
  const length = Math.max(nextParsed.prerelease.length, installed.prerelease.length)
  for (let index = 0; index < length; index++) {
    const nextId = nextParsed.prerelease[index]
    const installedId = installed.prerelease[index]
    if (nextId === installedId) continue
    if (nextId === void 0) return false
    if (installedId === void 0) return true
    const nextIsNumber = /^\d+$/.test(nextId)
    const installedIsNumber = /^\d+$/.test(installedId)
    if (nextIsNumber && installedIsNumber) return compareNumeric(nextId, installedId)
    if (nextIsNumber !== installedIsNumber) return installedIsNumber
    return nextId > installedId
  }
  return false
}
function fetchRegistry(urlOrTarget) {
  requestedUrls.push(urlOrTarget)
  return Promise.resolve({
    ok: true,
    json: async () => ({ latest: urlOrTarget === 'private-registry' ? '3.0.0' : '2.0.0' })
  })
}

const pkg = '`@quasar/cli`'
const registryCacheFile = getCacheFile(pkg)
writeCache(registryCacheFile, { checkedAt: Date.now(), latest: '2.0.0' })

const previousRegistry = process.env.npm_config_registry
process.env.npm_config_registry = 'http://private.example/registry/'
function checkFromCacheSim(cacheFile, pkg, version) {
  const cache = readCache(cacheFile)
  if (cache && cache.checkedAt && Date.now() - cache.checkedAt < 24 * 60 * 60 * 1000) return { cache, backgroundCheckScheduled: false }
  const cacheFileAfter = getCacheFile(pkg)
  return { cache, backgroundCheckScheduled: true, cacheFileAfter }
}

function checkFromFetchSim(cacheFile, pkg, version) {
  const registry = new URL(process.env.npm_config_registry || 'https://registry.npmjs.org')
  if (!registry.pathname.endsWith('/')) registry.pathname += '/'
  const packagePath = encodeURIComponent(pkg)
  const url = new URL(`-/package/${packagePath}/dist-tags`, registry)
  return fetchRegistry(registry.pathname + `-/package/${packagePath}/dist-tags`).then(async (response) => {
    const { latest } = await response.json()
    writeCache(cacheFile, {
      checkedAt: Date.now(),
      latest: isNewerVersion(latest, version) ? latest : void 0
    })
  })
}

const cache = readCache(registryCacheFile)
console.log(JSON.stringify({
  cacheKey: registryCacheFile,
  cachedLatestInitially: cache.latest,
  selectedRegistryConfig: process.env.npm_config_registry,
  selectedRegistryPathname: new URL(process.env.npm_config_registry).pathname,
  cachedCheckedAtAgeWithinInterval: cache && cache.checkedAt && Date.now() - cache.checkedAt < 24 * 60 * 60 * 1000,
  intervalTriggeredBackgroundCheck: false,
  fetchWouldHaveQueriedPathname: new URL(process.env.npm_config_registry).pathname,
}))

if (previousRegistry === undefined) delete process.env.npm_config_registry
else process.env.npm_config_registry = previousRegistry
rmSync(cacheRoot, { recursive: true, force: true })
JS

echo '--- lint check presence (if available without repo code execution) ---'
if [ -f package.json ]; then jq '.scripts // {}' package.json; fi

Repository: quasarframework/quasar

Length of output: 714


Partition update-notifier cache by registry.

getCacheFile() uses only package, while checkForUpdate() uses npm_config_registry. Switching registries can reuse another registry’s latest notification and delay the active registry check until checkedAt expires. Include the registry identity in the cache key, or persist/store it per package and ignore cache when it changes.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/update-notifier/src/internal.js` around lines 187 - 189, Update the
cache handling used by checkForUpdate and getCacheFile so cached update results
are partitioned by npm_config_registry. Include the registry identity in the
cache key or store and validate it with the cached package data, ignoring stale
cache entries when the active registry changes while preserving existing
behavior for the same registry.

@rstoenescu
rstoenescu merged commit 63d75dd into quasarframework:dev Aug 5, 2026
2 of 54 checks passed
@hawkeye64
hawkeye64 deleted the agent/publishable-update-notifier branch September 16, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants