From 03228fc878da4968d424c455c69cf0d607969b31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:51:27 +0000 Subject: [PATCH 1/3] Bump the dependencies group across 1 directory with 2 updates Bumps the dependencies group with 2 updates in the / directory: [eslint](https://github.com/eslint/eslint) and [mocha](https://github.com/mochajs/mocha). Updates `eslint` from 10.4.0 to 10.4.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.4.0...v10.4.1) Updates `mocha` from 11.7.5 to 11.7.6 - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/v11.7.6/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v11.7.5...v11.7.6) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.4.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: mocha dependency-version: 11.7.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- bun.lock | 10 +++++----- package.json | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index aefba78..9fe3af3 100644 --- a/bun.lock +++ b/bun.lock @@ -7,10 +7,10 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@lando/leia": "^1.0.0-beta.4", - "eslint": "^10.4.0", + "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.1", "globals": "^17.6.0", - "mocha": "^11.7.5", + "mocha": "^11.7.6", "prettier": "^3.8.3", "shellcheck": "^4.1.0", }, @@ -33,7 +33,7 @@ "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="], "@felipecrs/decompress-tarxz": ["@felipecrs/decompress-tarxz@5.0.4", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.1.0", "file-type": "^20.5.0", "is-stream": "^2.0.1", "xz-decompress": "^0.2.3" } }, "sha512-a+nAnDsiUA84Sy/a+FKYJtjOjFvNtW8Jcbi3NwE8kJKPpYAxINFLYsC9mev9/wngiNEBA3jfHn0qNFwICeZNJw=="], @@ -239,7 +239,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], + "eslint": ["eslint@10.4.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], @@ -431,7 +431,7 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "mocha": ["mocha@11.7.5", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", "ms": "^2.1.3", "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig=="], + "mocha": ["mocha@11.7.6", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", "ms": "^2.1.3", "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], diff --git a/package.json b/package.json index 5ddd624..a5e383c 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@lando/leia": "^1.0.0-beta.4", - "eslint": "^10.4.0", + "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.1", "globals": "^17.6.0", - "mocha": "^11.7.5", + "mocha": "^11.7.6", "prettier": "^3.8.3", "shellcheck": "^4.1.0" }, From 8976f9b366caf04a78047152f50c0335620c0500 Mon Sep 17 00:00:00 2001 From: Mike Pirog Date: Wed, 3 Jun 2026 11:20:58 -0400 Subject: [PATCH 2/3] brewupdate before tests for up2date metadata --- .github/workflows/pr-examples-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-examples-tests.yml b/.github/workflows/pr-examples-tests.yml index 16865c3..760aa80 100644 --- a/.github/workflows/pr-examples-tests.yml +++ b/.github/workflows/pr-examples-tests.yml @@ -50,6 +50,7 @@ jobs: TMPDIR: ${{ github.workspace }}/examples/${{ matrix.example }}/.tmp OPTOKEN: ${{ secrets.TANAAB_OP_TEST }} run: | + brew update mkdir -p "$TMPDIR" bun run leia "examples/${{ matrix.example }}/README.md" -c "Destroy tests" --stdin From 8276bc360df44d59a6caff7b1e40d150b7f00fcb Mon Sep 17 00:00:00 2001 From: Mike Pirog Date: Wed, 3 Jun 2026 11:36:10 -0400 Subject: [PATCH 3/3] #25: update zsh startup and node readiness --- .tool-versions | 2 +- Brewfile | 2 +- dotfiles/zsh/.zprofile | 13 +- dotfiles/zsh/.zshrc | 121 ++--------------- .../me-readiness/scripts/check-machine-lib.js | 127 +++++++++++++++++- test/me-readiness-check-machine.spec.js | 115 +++++++++++++++- 6 files changed, 254 insertions(+), 126 deletions(-) diff --git a/.tool-versions b/.tool-versions index cae58f5..f75e275 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ bun 1.3 -nodejs 20 +nodejs 24 diff --git a/Brewfile b/Brewfile index f784cce..78530b6 100644 --- a/Brewfile +++ b/Brewfile @@ -9,7 +9,7 @@ brew "gh" brew "git" brew "imagemagick" brew "jq" -brew "node@20" +brew "node@24" brew "python@3.14" brew "stow" brew "zsh" diff --git a/dotfiles/zsh/.zprofile b/dotfiles/zsh/.zprofile index 3ef6d93..1c94960 100644 --- a/dotfiles/zsh/.zprofile +++ b/dotfiles/zsh/.zprofile @@ -1,17 +1,8 @@ -if [[ "$(/usr/bin/uname -m)" == "arm64" ]] && [[ -x /opt/homebrew/bin/brew ]]; then +# Load Homebrew for login shells. +if [[ -x /opt/homebrew/bin/brew ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [[ -x /usr/local/bin/brew ]]; then eval "$(/usr/local/bin/brew shellenv)" elif command -v brew >/dev/null 2>&1; then eval "$(brew shellenv)" fi - -# Setting PATH for Python 3.10 -# The original version is saved in .zprofile.pysave -#PATH="/Library/Frameworks/Python.framework/Versions/3.10/bin:${PATH}" -#export PATH - -# Setting PATH for Python 2.7 -# The original version is saved in .zprofile.pysave -#PATH="/Library/Frameworks/Python.framework/Versions/2.7/bin:${PATH}" -#export PATH diff --git a/dotfiles/zsh/.zshrc b/dotfiles/zsh/.zshrc index 57dd979..4e1c1f4 100644 --- a/dotfiles/zsh/.zshrc +++ b/dotfiles/zsh/.zshrc @@ -1,17 +1,11 @@ -# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. -# Initialization code that may require console input (password prompts, [y/n] -# confirmations, etc.) must go above this block; everything else may go below. +# Enable Powerlevel10k instant prompt. Keep this close to the top of ~/.zshrc. if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" fi -# If you come from bash you might have to change your $PATH. -# export PATH=$HOME/bin:/usr/local/bin:$PATH - # Some terminal apps launch interactive non-login shells, which skip ~/.zprofile. -# Rehydrate Homebrew here only when it is not already configured. if [[ -z "${HOMEBREW_PREFIX:-}" ]]; then - if [[ "$(/usr/bin/uname -m)" == "arm64" ]] && [[ -x /opt/homebrew/bin/brew ]]; then + if [[ -x /opt/homebrew/bin/brew ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [[ -x /usr/local/bin/brew ]]; then eval "$(/usr/local/bin/brew shellenv)" @@ -20,109 +14,13 @@ if [[ -z "${HOMEBREW_PREFIX:-}" ]]; then fi fi -# Path to your oh-my-zsh installation. export ZSH="$HOME/.oh-my-zsh" - -# Set name of the theme to load --- if set to "random", it will -# load a random theme each time oh-my-zsh is loaded, in which case, -# to know which specific one was loaded, run: echo $RANDOM_THEME -# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes ZSH_THEME="powerlevel10k/powerlevel10k" - -# Set list of themes to pick from when loading at random -# Setting this variable when ZSH_THEME=random will cause zsh to load -# a theme from this variable instead of looking in $ZSH/themes/ -# If set to an empty array, this variable will have no effect. -# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) - -# Uncomment the following line to use case-sensitive completion. -# CASE_SENSITIVE="true" - -# Uncomment the following line to use hyphen-insensitive completion. -# Case-sensitive completion must be off. _ and - will be interchangeable. -# HYPHEN_INSENSITIVE="true" - -# Uncomment the following line to disable bi-weekly auto-update checks. -# DISABLE_AUTO_UPDATE="true" - -# Uncomment the following line to automatically update without prompting. -# DISABLE_UPDATE_PROMPT="true" - -# Uncomment the following line to change how often to auto-update (in days). -# export UPDATE_ZSH_DAYS=13 - -# Uncomment the following line if pasting URLs and other text is messed up. -# DISABLE_MAGIC_FUNCTIONS="true" - -# Uncomment the following line to disable colors in ls. -# DISABLE_LS_COLORS="true" - -# Uncomment the following line to disable auto-setting terminal title. -# DISABLE_AUTO_TITLE="true" - -# Uncomment the following line to enable command auto-correction. -# ENABLE_CORRECTION="true" - -# Uncomment the following line to display red dots whilst waiting for completion. -# Caution: this setting can cause issues with multiline prompts (zsh 5.7.1 and newer seem to work) -# See https://github.com/ohmyzsh/ohmyzsh/issues/5765 -# COMPLETION_WAITING_DOTS="true" - -# Uncomment the following line if you want to disable marking untracked files -# under VCS as dirty. This makes repository status check for large repositories -# much, much faster. -# DISABLE_UNTRACKED_FILES_DIRTY="true" - -# Uncomment the following line if you want to change the command execution time -# stamp shown in the history command output. -# You can set one of the optional three formats: -# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" -# or set a custom format using the strftime function format specifications, -# see 'man strftime' for details. -# HIST_STAMPS="mm/dd/yyyy" - -# Would you like to use another custom folder than $ZSH/custom? -# ZSH_CUSTOM=/path/to/new-custom-folder - -# Which plugins would you like to load? -# Standard plugins can be found in $ZSH/plugins/ -# Custom plugins may be added to $ZSH_CUSTOM/plugins/ -# Example format: plugins=(rails git textmate ruby lighthouse) -# Add wisely, as too many plugins slow down shell startup. plugins=(git zsh-autosuggestions) source "$ZSH/oh-my-zsh.sh" -# User configuration - -# export MANPATH="/usr/local/man:$MANPATH" - -# You may need to manually set your language environment -# export LANG=en_US.UTF-8 - -# Preferred editor for local and remote sessions -# if [[ -n $SSH_CONNECTION ]]; then -# export EDITOR='vim' -# else -# export EDITOR='mvim' -# fi - -# Compilation flags -# export ARCHFLAGS="-arch x86_64" - -# Set personal aliases, overriding those provided by oh-my-zsh libs, -# plugins, and themes. Aliases can be placed here, though oh-my-zsh -# users are encouraged to define aliases within the ZSH_CUSTOM folder. -# For a full list of active aliases, run `alias`. -# -# Example aliases -# alias zshconfig="mate ~/.zshrc" -# alias ohmyzsh="mate ~/.oh-my-zsh" - -# export PIROG=true #piroline: - -# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh. -[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh +[[ ! -f "$HOME/.p10k.zsh" ]] || source "$HOME/.p10k.zsh" if [[ -r "$HOME/.oh-my-zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" ]]; then source "$HOME/.oh-my-zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" @@ -130,19 +28,22 @@ elif [[ -r "${ZSH_CUSTOM:-$ZSH/custom}/plugins/zsh-syntax-highlighting/zsh-synta source "${ZSH_CUSTOM:-$ZSH/custom}/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" fi -export PATH="$HOME/.lando/bin:$PATH" #landopath +export PATH="$HOME/.lando/bin:$PATH" -# bun completions -[[ -s "$HOME/.bun/_bun" ]] && source "$HOME/.bun/_bun" +if [[ -s "$HOME/.bun/_bun" ]]; then + source "$HOME/.bun/_bun" +fi -# bun export BUN_INSTALL="$HOME/.bun" export PATH="$BUN_INSTALL/bin:$PATH" -# The following lines have been added by Docker Desktop to enable Docker CLI completions. if [[ -d "$HOME/.docker/completions" ]]; then fpath=("$HOME/.docker/completions" $fpath) fi autoload -Uz compinit compinit + +if [[ -n "${HOMEBREW_PREFIX:-}" && -d "${HOMEBREW_PREFIX}/opt/node@24/bin" ]]; then + export PATH="${HOMEBREW_PREFIX}/opt/node@24/bin:$PATH" +fi diff --git a/skills/me-readiness/scripts/check-machine-lib.js b/skills/me-readiness/scripts/check-machine-lib.js index d2e894a..2985f4c 100644 --- a/skills/me-readiness/scripts/check-machine-lib.js +++ b/skills/me-readiness/scripts/check-machine-lib.js @@ -21,15 +21,18 @@ const READINESS_AUTHORIZATION_CODE_KEY = 'READINESS_AUTHORIZATION_CODE'; const EXPECTED_READINESS_AUTHORIZATION_CODE_SHA256 = 'a924fd4b1d47841c36ae7663db374cf040b913ffa56541fe0f345435e3cce267'; const ONEPASSWORD_ENVIRONMENT_VALIDATION_SCRIPT = `import { createHash } from "node:crypto";const value=process.env.${READINESS_AUTHORIZATION_CODE_KEY};const hash=value?createHash("sha256").update(value).digest("hex"):"";process.stdout.write(JSON.stringify({present:Boolean(value),matches:hash==="${EXPECTED_READINESS_AUTHORIZATION_CODE_SHA256}"}));`; +const EXPECTED_NODE_FORMULA = 'node@24'; +const EXPECTED_NODE_MAJOR_VERSION = 24; export const REQUIRED_BREWFILE_CASKS = ['1password', '1password-cli@beta', 'tailscale']; +export const REQUIRED_BREWFILE_FORMULAS = [EXPECTED_NODE_FORMULA]; export const FORBIDDEN_BREWFILE_CASKS = [ { cask: '1password-cli', id: 'brewfile_cask_1password_cli_stable_absent', }, ]; -export const REQUIRED_COMMANDS = ['brew', 'bun', 'git', 'gh', 'op', 'stow', 'tailscale']; +export const REQUIRED_COMMANDS = ['brew', 'bun', 'git', 'gh', 'node', 'op', 'stow', 'tailscale']; export const ONEPASSWORD_TOKEN_ENV_KEYS = [ 'PIROME_OP_TOKEN', 'TANAAB_OP_TOKEN', @@ -103,7 +106,13 @@ const CHECK_BUCKET_BY_ID = new Map([ ]), ['brewfile_readable', 'packages'], ...REQUIRED_BREWFILE_CASKS.map((cask) => [`brewfile_cask_${checkIdSegment(cask)}`, 'packages']), + ...REQUIRED_BREWFILE_FORMULAS.map((formula) => [ + `brewfile_formula_${checkIdSegment(formula)}`, + 'packages', + ]), ...FORBIDDEN_BREWFILE_CASKS.map(({ id }) => [id, 'packages']), + ['node_homebrew_path', 'packages'], + ['node_version', 'packages'], ['vimrc_link', 'dotfiles'], ['vimrc_before_link', 'dotfiles'], ['vimrc_after_link', 'dotfiles'], @@ -162,6 +171,13 @@ function hasCask(brewfile, cask) { ).test(brewfile); } +function hasFormula(brewfile, formula) { + return new RegExp( + `^\\s*brew\\s+["']${formula.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']`, + 'm', + ).test(brewfile); +} + function formatMode(mode) { return `0${(mode & 0o777).toString(8)}`; } @@ -462,6 +478,21 @@ async function appendBrewfileChecks(checks, repoRoot, deps) { ); } + for (const formula of REQUIRED_BREWFILE_FORMULAS) { + checks.push( + hasFormula(brewfile, formula) + ? pass( + `brewfile_formula_${checkIdSegment(formula)}`, + `Brewfile includes formula "${formula}".`, + ) + : fail( + `brewfile_formula_${checkIdSegment(formula)}`, + `Brewfile does not include formula "${formula}".`, + 'Update the Brewfile and rerun https://boot.pirog.me/boot.sh or install the missing Brewfile dependency.', + ), + ); + } + for (const { cask, id } of FORBIDDEN_BREWFILE_CASKS) { checks.push( hasCask(brewfile, cask) @@ -475,6 +506,99 @@ async function appendBrewfileChecks(checks, repoRoot, deps) { } } +async function appendNodeRuntimeChecks(checks, deps) { + const brewCheck = getCheck(checks, 'command_brew'); + const nodeCheck = getCheck(checks, 'command_node'); + const formulaLabel = EXPECTED_NODE_FORMULA; + const remediation = + 'Install node@24 from the Brewfile, ensure Homebrew shellenv loads first, and put "$(brew --prefix node@24)/bin" before other node providers on PATH.'; + + if (brewCheck?.status === 'fail') { + checks.push( + fail( + 'node_homebrew_path', + 'Homebrew node path could not be checked because brew is missing.', + 'Install Homebrew, rerun https://boot.pirog.me/boot.sh, then rerun the readiness helper.', + ), + fail( + 'node_version', + 'Node version could not be checked because brew is missing.', + 'Install Homebrew and node@24 from the Brewfile, then rerun the readiness helper.', + ), + ); + return; + } + + if (nodeCheck?.status === 'fail') { + checks.push( + fail( + 'node_homebrew_path', + 'Homebrew node path could not be checked because node is missing.', + remediation, + ), + fail( + 'node_version', + 'Node version could not be checked because node is missing.', + remediation, + ), + ); + return; + } + + let expectedNodePath = ''; + + try { + const { stdout } = await deps.execFile('brew', ['--prefix', formulaLabel]); + const nodePrefix = stdout.trim(); + expectedNodePath = path.join(nodePrefix, 'bin', 'node'); + } catch { + checks.push( + fail( + 'node_homebrew_path', + `Homebrew prefix for ${formulaLabel} could not be resolved.`, + remediation, + ), + ); + } + + if (expectedNodePath) { + try { + const { stdout } = await deps.execFile('which', ['node']); + const actualNodePath = stdout.trim(); + + checks.push( + actualNodePath === expectedNodePath + ? pass('node_homebrew_path', `node resolves to ${formulaLabel} at ${actualNodePath}.`) + : fail( + 'node_homebrew_path', + `node resolves to ${actualNodePath || 'an empty path'}, expected ${expectedNodePath}.`, + remediation, + ), + ); + } catch { + checks.push(fail('node_homebrew_path', 'which node failed.', remediation)); + } + } + + try { + const { stdout } = await deps.execFile('node', ['--version']); + const version = stdout.trim(); + const major = Number.parseInt(version.replace(/^v/, '').split('.')[0] ?? '', 10); + + checks.push( + major === EXPECTED_NODE_MAJOR_VERSION + ? pass('node_version', `node reports version ${version}.`) + : fail( + 'node_version', + `node reports version ${version || 'unknown'}, expected major version ${EXPECTED_NODE_MAJOR_VERSION}.`, + remediation, + ), + ); + } catch { + checks.push(fail('node_version', 'node --version failed.', remediation)); + } +} + async function appendRequiredCommandChecks(checks, deps) { for (const command of REQUIRED_COMMANDS.filter((requiredCommand) => requiredCommand !== 'brew')) { checks.push(await commandCheck(command, deps)); @@ -696,6 +820,7 @@ export async function checkMachine(options = {}) { checks.push(await commandCheck('brew', deps)); await appendBrewfileChecks(checks, repoRoot, deps); await appendRequiredCommandChecks(checks, deps); + await appendNodeRuntimeChecks(checks, deps); await appendStowedLinkChecks(checks, DOTFILE_LINKS, homeDir, deps); await appendVimJanusRuntimeCheck(checks, homeDir, deps); await appendGeneratedConfigCheck(checks, homeDir, deps); diff --git a/test/me-readiness-check-machine.spec.js b/test/me-readiness-check-machine.spec.js index 7b564f5..83bd95d 100644 --- a/test/me-readiness-check-machine.spec.js +++ b/test/me-readiness-check-machine.spec.js @@ -40,6 +40,9 @@ function makeHealthyTailscaleStatus(overrides = {}) { }; } +const NODE_BREW_PREFIX = '/opt/homebrew/opt/node@24'; +const NODE_BREW_PATH = path.join(NODE_BREW_PREFIX, 'bin', 'node'); + function healthyExistingPaths(...missingPaths) { const missing = new Set(missingPaths); @@ -60,7 +63,13 @@ function healthyExistingPaths(...missingPaths) { } function makeDeps({ - brewfile = ['cask "1password"', 'cask "1password-cli@beta"', 'cask "tailscale"'].join('\n'), + brewfile = [ + 'cask "1password"', + 'cask "1password-cli@beta"', + 'cask "tailscale"', + 'brew "node@24"', + ].join('\n'), + brewPrefixError = false, commands = REQUIRED_COMMANDS, configMode = 0o100600, environmentCliHelp = true, @@ -74,11 +83,15 @@ function makeDeps({ existingPaths, opExecError = false, opEnvironmentHelpError = false, + nodePath = NODE_BREW_PATH, + nodeVersion = 'v24.11.1', + nodeVersionError = false, symbolicLinks, tailscaleExecError = false, tailscaleStatus = makeHealthyTailscaleStatus(), tailscaleStdout, vaults = [{ id: 'vault' }], + whichNodeError = false, } = {}) { const existing = new Set(existingPaths ?? healthyExistingPaths()); const symlinks = new Set( @@ -101,6 +114,40 @@ function makeDeps({ execFile(command, args, options = {}) { execCalls?.push({ args, command, options }); + if (command === 'brew') { + assert.deepEqual(args, ['--prefix', 'node@24']); + + if (brewPrefixError) { + throw brewPrefixError instanceof Error + ? brewPrefixError + : new Error('brew prefix failed'); + } + + return { stdout: `${NODE_BREW_PREFIX}\n` }; + } + + if (command === 'which') { + assert.deepEqual(args, ['node']); + + if (whichNodeError) { + throw whichNodeError instanceof Error ? whichNodeError : new Error('which node failed'); + } + + return { stdout: `${nodePath}\n` }; + } + + if (command === 'node') { + assert.deepEqual(args, ['--version']); + + if (nodeVersionError) { + throw nodeVersionError instanceof Error + ? nodeVersionError + : new Error('node version failed'); + } + + return { stdout: `${nodeVersion}\n` }; + } + if (command === 'op') { if (args[0] === 'vault') { assert.deepEqual(args, ['vault', 'list', '--format', 'json']); @@ -214,6 +261,23 @@ describe('skills/me-readiness/scripts/check-machine-lib', () => { assert.equal(report.checks[0].bucket, 'homebrew'); }); + it('should verify Homebrew node@24 is the active node runtime', async () => { + const report = await runCheck({ + existingPaths: healthyExistingPaths(), + }); + const formulaCheck = report.checks.find((check) => check.id === 'brewfile_formula_node_24'); + const commandCheck = report.checks.find((check) => check.id === 'command_node'); + const pathCheck = report.checks.find((check) => check.id === 'node_homebrew_path'); + const versionCheck = report.checks.find((check) => check.id === 'node_version'); + + assert.equal(formulaCheck.status, 'pass'); + assert.equal(commandCheck.status, 'pass'); + assert.equal(pathCheck.status, 'pass'); + assert.equal(versionCheck.status, 'pass'); + assert.match(pathCheck.message, /node@24/); + assert.match(versionCheck.message, /v24/); + }); + it('should assign stable buckets in readiness order', async () => { const report = await runCheck({ existingPaths: healthyExistingPaths(), @@ -268,6 +332,7 @@ describe('skills/me-readiness/scripts/check-machine-lib', () => { assert.ok(report.checks.some((check) => check.id === 'brewfile_cask_1password')); assert.ok(report.checks.some((check) => check.id === 'brewfile_cask_1password_cli_beta')); + assert.ok(report.checks.some((check) => check.id === 'brewfile_formula_node_24')); assert.ok( report.checks.some((check) => check.id === 'brewfile_cask_1password_cli_stable_absent'), ); @@ -327,7 +392,12 @@ describe('skills/me-readiness/scripts/check-machine-lib', () => { it('should require the beta 1Password CLI cask and reject the stable cask', async () => { const report = await runCheck({ - brewfile: ['cask "1password"', 'cask "1password-cli"', 'cask "tailscale"'].join('\n'), + brewfile: [ + 'cask "1password"', + 'cask "1password-cli"', + 'cask "tailscale"', + 'brew "node@24"', + ].join('\n'), existingPaths: healthyExistingPaths(), }); const betaCheck = report.checks.find( @@ -342,6 +412,47 @@ describe('skills/me-readiness/scripts/check-machine-lib', () => { assert.match(stableCheck.remediation, /1password-cli@beta/); }); + it('should require the node@24 Brewfile formula', async () => { + const report = await runCheck({ + brewfile: ['cask "1password"', 'cask "1password-cli@beta"', 'cask "tailscale"'].join('\n'), + existingPaths: healthyExistingPaths(), + }); + const formulaCheck = report.checks.find((check) => check.id === 'brewfile_formula_node_24'); + + assert.equal(report.ok, false); + assert.equal(formulaCheck.status, 'fail'); + assert.match(formulaCheck.message, /node@24/); + assert.match(formulaCheck.remediation, /Brewfile/); + }); + + it('should fail when node resolves outside Homebrew node@24', async () => { + const report = await runCheck({ + existingPaths: healthyExistingPaths(), + nodePath: '/Users/tester/.asdf/shims/node', + }); + const pathCheck = report.checks.find((check) => check.id === 'node_homebrew_path'); + + assert.equal(report.ok, false); + assert.equal(pathCheck.status, 'fail'); + assert.match(pathCheck.message, /asdf/); + assert.match(pathCheck.message, /node@24/); + assert.match(pathCheck.remediation, /brew --prefix node@24/); + }); + + it('should fail when node does not report major version 24', async () => { + const report = await runCheck({ + existingPaths: healthyExistingPaths(), + nodeVersion: 'v23.11.0', + }); + const versionCheck = report.checks.find((check) => check.id === 'node_version'); + + assert.equal(report.ok, false); + assert.equal(versionCheck.status, 'fail'); + assert.match(versionCheck.message, /v23\.11\.0/); + assert.match(versionCheck.message, /24/); + assert.match(versionCheck.remediation, /node@24/); + }); + it('should pass Vim readiness when stowed links and Janus runtime exist', async () => { const report = await runCheck({ existingPaths: healthyExistingPaths(),