-
Notifications
You must be signed in to change notification settings - Fork 455
Proj/setting auto update #696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aaaacd6
2f513cd
18ef775
b7f1a8b
f04f11c
bffc10d
e5bd2ce
4ed2bd4
b286955
0fb18b7
c93d67d
2967eff
db66086
ecf2da6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import type {Hook} from '@oclif/core' | ||
|
|
||
| import {checkForUpdatesSetting} from '../../lib/check-for-updates-setting.js' | ||
|
|
||
| export type BlockAutoupdateWhenOffDeps = { | ||
| argv: readonly string[] | ||
| commandId: string | undefined | ||
| exitFn: (code: number) => never | ||
| } | ||
|
|
||
| /** | ||
| * Short-circuits `brv update --autoupdate` (the background invocation | ||
| * `@oclif/plugin-update` spawns on a debounce) when the user has set | ||
| * `update.checkForUpdates` to `false`. Exits silently with code 0 so | ||
| * plugin-update sees a normal-looking termination rather than an error | ||
| * it might retry or log. | ||
| * | ||
| * The user-initiated `brv update` (no `--autoupdate` flag) is **not** | ||
| * blocked — that remains the user's manual escape hatch. | ||
| */ | ||
| export function handleBlockAutoupdateWhenOff(deps: BlockAutoupdateWhenOffDeps): void { | ||
| if (deps.commandId !== 'update') return | ||
| if (!deps.argv.includes('--autoupdate')) return | ||
| if (checkForUpdatesSetting()) return | ||
| deps.exitFn(0) | ||
| } | ||
|
|
||
| const hook: Hook<'init'> = function (opts): Promise<void> { | ||
| handleBlockAutoupdateWhenOff({ | ||
| argv: opts.argv, | ||
| commandId: opts.id, | ||
| exitFn: process.exit, | ||
| }) | ||
| return Promise.resolve() | ||
| } | ||
|
|
||
| export default hook | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ import {confirm} from '@inquirer/prompts' | |
| import {execSync, spawn} from 'node:child_process' | ||
| import updateNotifier from 'update-notifier' | ||
|
|
||
| import {checkForUpdatesSetting} from '../../lib/check-for-updates-setting.js' | ||
|
|
||
| /** | ||
| * Check interval for update notifications (1 hour) | ||
| */ | ||
|
|
@@ -46,7 +48,29 @@ export const isNpmGlobalInstall = (execSyncFn: typeof execSync): boolean => { | |
| } | ||
|
|
||
| /** | ||
| * Core update notification logic, extracted for testability | ||
| * Resolves whether the init hook should run the update check at all. | ||
| * | ||
| * Returns `false` when any of the following holds: | ||
| * - The `update.checkForUpdates` setting is explicitly `false`. | ||
| * - The command being invoked is itself `update` (anti-recursion). | ||
| * - `BRV_ENV` is `development` (dev installs handle updates manually). | ||
| * | ||
| * Returns `true` otherwise (default behaviour). | ||
| */ | ||
| export function shouldRunUpdateCheck(args: {commandId: string | undefined}): boolean { | ||
| if (args.commandId === 'update') return false | ||
| if (process.env.BRV_ENV === 'development') return false | ||
| return checkForUpdatesSetting() | ||
| } | ||
|
Comment on lines
+60
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. praise: Clean three-condition gate with explicit short-circuit ordering — |
||
|
|
||
| /** | ||
| * Core update notification logic, extracted for testability. | ||
| * | ||
| * Interactive flow only: shows a confirm prompt, then runs `npm update -g` | ||
| * + `brv restart` if the user types `y`. There is **no** unattended / | ||
| * automatic execution path — that was deliberately rejected by the team | ||
| * (May 2026). When the user types `n` (or the prompt is unavailable), | ||
| * the hook just notifies and returns. | ||
| */ | ||
| export async function handleUpdateNotification(deps: UpdateNotifierDeps): Promise<void> { | ||
| const {confirmPrompt, execSyncFn, exitFn, isNpmGlobalInstalled, isTTY, log, notifier} = deps | ||
|
|
@@ -89,7 +113,9 @@ export async function handleUpdateNotification(deps: UpdateNotifierDeps): Promis | |
| } | ||
| } | ||
|
|
||
| const hook: Hook<'init'> = async function (): Promise<void> { | ||
| const hook: Hook<'init'> = async function (opts): Promise<void> { | ||
| if (!shouldRunUpdateCheck({commandId: opts.id})) return | ||
|
|
||
| const pkgInfo = {name: this.config.name, version: this.config.version} | ||
| const notifier = updateNotifier({pkg: pkgInfo, updateCheckInterval: UPDATE_CHECK_INTERVAL_MS}) | ||
| const isNpmGlobalInstalled = isNpmGlobalInstall(execSync) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {existsSync, readFileSync} from 'node:fs' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {join} from 'node:path' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {getGlobalDataDir} from '../../server/utils/global-data-path.js' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const SETTINGS_KEY = 'update.checkForUpdates' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const SETTINGS_FILENAME = 'settings.json' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Synchronous read of the user's `update.checkForUpdates` setting. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Default is `true` (update check enabled). Returns `false` only when the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * persisted settings file explicitly contains | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `{values: {"update.checkForUpdates": false}}`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Lives in `oclif/lib/` because the only consumers today are oclif init | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * hooks that must run before the daemon connection is established and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * therefore cannot read the setting via the `settings:list` transport | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * event. Path resolution reuses `getGlobalDataDir()` so the helper lands | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * on the exact same file the daemon writes to. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function checkForUpdatesSetting(): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = join(getGlobalDataDir(), SETTINGS_FILENAME) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!existsSync(path)) return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed: unknown = JSON.parse(readFileSync(path, 'utf8')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick (non-blocking): Strictly optional: add an |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(parsed)) return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(parsed.values)) return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const value = parsed.values[SETTINGS_KEY] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value !== 'boolean') return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (non-blocking): The fallback values ( Per CLAUDE.md: "Descriptors in
Suggested change
Note: the suggestion above adds an import that this file doesn't currently have — apply the import line separately if you accept it. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return typeof value === 'object' && value !== null && !Array.isArray(value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question (non-blocking): Hook ordering — the PR description (and
package.json:159-164) registersblock-autoupdate-when-offbeforeblock-command-update-npm, which is critical: if reversed, the npm-install + setting-off + autoupdate path would log a noisy exit-1 instead of the intended silent exit.There's no test guard that asserts this ordering. Given the risk you've already documented in the PR body ("If a future refactor reorders the hooks..."), would it be worth adding an integration-style test that reads
package.jsonand assertsblock-autoupdate-when-offprecedesblock-command-update-npminoclif.hooks.init? Cheap to write, gives you a CI tripwire if anyone alphabetizes or reorders the array.