Skip to content

fix(node/nix): consolidate workspace lockfile + update all consumers - #36171

Merged
ethernet8023 merged 1 commit into
mainfrom
ethie/lockfile
Jun 3, 2026
Merged

fix(node/nix): consolidate workspace lockfile + update all consumers#36171
ethernet8023 merged 1 commit into
mainfrom
ethie/lockfile

Conversation

@ethernet8023

@ethernet8023 ethernet8023 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Consolidates the npm workspace from per-package package-lock.json files to a single root-level lockfile, updates all consumers (Python CLI, Nix, Dockerfile, CI) to resolve against the workspace root, and replaces the desktop --force-build/mtime heuristic with a content-hash build stamp.

Changes Made

Workspace lockfile consolidation

  • package.json: Added ui-tui, ui-tui/packages/*, and web to the workspaces array so npm install at the repo root resolves all packages from a single hoisted node_modules/.
  • nix/lib.nix: Centralized src, npmDeps, npmDepsHash, and npmDepsFetcherVersion into shared helpers; mkNpmPassthru now returns src, npmDeps, npmDepsFetcherVersion, npmRoot, packageJsonPath, and shared npmFlags — individual .nix files no longer duplicate these. mkNpmDevShellHook stamps all workspace package.json files, runs npm i --package-lock-only if any changed, then npm ci if the lockfile changed.
  • nix/desktop.nix: Removed per-package fetchNpmDeps and src; uses shared npm attrs from mkNpmPassthru; sourceRoot dropped in favour of cd apps/desktop in build phase; removed --legacy-peer-deps (workspace lockfile resolves peers correctly); removed substituteInPlace for alias rewriting (no longer needed).
  • nix/tui.nix: Removed per-package fetchNpmDeps and src; uses shared npm attrs; added explicit buildPhase running node ui-tui/scripts/build.mjs from workspace root.
  • nix/web.nix: Removed per-package fetchNpmDeps and src; uses shared npm attrs; build cds into web/ so vite.config.ts and tsconfig resolve correctly; calls tsc and vite via node ../node_modules/... instead of npx.
  • nix/packages.nix: Simplified fix-lockfiles to pass attr = "tui" instead of enumerating all packages.
  • nix/devShell.nix: Replaced per-package devShellHook collection with packageJsonPath collection; non-npm packages still expose their own devShellHook; npm setup is now a single mkNpmDevShellHook call.
  • Deleted lockfiles: apps/desktop/package-lock.json, ui-tui/package-lock.json, ui-tui/packages/hermes-ink/package-lock.json, web/package-lock.json.

Python CLI workspace-root awareness

  • hermes_cli/main.py:
    • New _workspace_root(dir) helper: returns the directory containing package-lock.json (either dir itself for standalone layouts, or dir.parent for workspace members). Used by _tui_need_npm_install, _make_tui_argv, and _build_web_ui so lockfile/node_modules resolution and npm install cwd are consistent.
    • _tui_need_npm_install: lockfile, ink, and marker checks now resolve from _workspace_root(root) instead of root directly.
    • _make_tui_argv / TUI launch: npm install runs from _workspace_root(tui_dir).
    • _build_web_ui: npm install runs from _workspace_root(web_dir).
    • _web_ui_build_needed: also checks the root package-lock.json mtime (single workspace lockfile covers all workspaces).
    • Removed import hashlib (moved to desktop-stamp section).

Desktop content-hash build stamp

  • hermes_cli/main.py:
    • New _compute_desktop_content_hash(project_root): SHA-256 of apps/desktop/ source tree plus root package.json/package-lock.json, respecting .gitignore via pathspec.
    • New _desktop_build_needed / _write_desktop_build_stamp: content-hash based skip logic replaces the old mtime heuristic.
    • Removed --force-build CLI flag (content-hash makes it unnecessary — hermes update calls --build-only unconditionally and the stamp skips if nothing changed).
    • Added pathspec to pyproject.toml dependencies for .gitignore-aware hashing.

Dockerfile

  • Removed per-directory package-lock.json copies (web/package-lock.json, ui-tui/package-lock.json).
  • Removed sub-directory npm install calls for web/ and ui-tui/; single root npm install covers all workspaces.

CI workflows

  • .github/workflows/nix-lockfile-fix.yml: Trigger paths updated from per-package lockfiles to root package-lock.json; apps/dashboard references corrected to apps/desktop; rebase staleness check uses root lockfile path.
  • .github/workflows/osv-scanner.yml: Removed ui-tui/package-lock.json from trigger and scan args; scans root package-lock.json instead.

Tests

  • tests/hermes_cli/test_tui_npm_install.py (new): tests for _workspace_root and _tui_need_npm_install with workspace-root lockfile layout.
  • tests/hermes_cli/test_gui_command.py: Removed --force-build test; added content-hash stamp tests (test_desktop_build_stamp_skips_build_when_up_to_date, test_desktop_force_build_overrides_stamp, test_compute_desktop_content_hash_stable, test_compute_desktop_content_hash_changes_on_edit, test_desktop_build_needed_detects_missing_artifact, test_desktop_build_stamp_round_trip, test_compute_desktop_content_hash_works_without_gitignore, test_compute_desktop_content_hash_respects_gitignore).
  • tests/hermes_cli/test_cmd_update.py: Updated npm-call assertions: repo-root install now covers all workspaces; ui-tui sub-directory install removed; web npm ci runs from workspace root.
  • tests/hermes_cli/test_web_ui_build.py: package-lock.json fixture moved from web_dir/ to tmp_path/ (workspace root).
  • tests/tools/test_dockerfile_pid1_reaping.py: Updated assertions: no ui-tui/package-lock.json COPY step; root package-lock.json is the single source of truth.

Docs

  • website/docs/user-guide/desktop.md: Removed --force-build row from the flag table; reordered --build-only description.

How to Test

  1. nix build .#tui — should build successfully from the single workspace lockfile
  2. nix build .#web — should build successfully
  3. nix build .#desktop — should build successfully
  4. nix run .#fix-lockfiles -- --check — should report no stale hashes
  5. Verify no per-package lockfiles remain: ls apps/desktop/package-lock.json ui-tui/package-lock.json web/package-lock.json — all should be gone
  6. Enter devshell, touch a package.json, re-enter — npm hook should detect the change
  7. Enter devshell, touch package-lock.json, re-enter — root hook should run npm ci + fix-lockfiles
  8. hermes desktop --build-only twice — second run should skip (content hash matches)

Type of Change

  • ♻️ Refactor (no behavior change)

@ethernet8023
ethernet8023 requested a review from a team June 1, 2026 01:25
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: ethie/lockfile vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9683 on HEAD, 9683 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5016 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change P2 Medium — degraded but workaround exists area/nix Nix flake, NixOS module, container packaging labels Jun 1, 2026

@mxnstrexgl mxnstrexgl 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.

Note: Large lockfile-only PR — verify no drift

3184 additions and 38221 deletions across lockfiles. The result-1 and result-2 symlinks in the changed paths look like Nix build artifacts — these should not be in the repo.

Check: Remove result-1 and result-2 from the PR. Verify npm install produces identical lockfile on clean checkout.

@ethernet8023
ethernet8023 force-pushed the ethie/lockfile branch 2 times, most recently from 35c3e5f to 133ee7a Compare June 1, 2026 13:45
@ethernet8023
ethernet8023 marked this pull request as draft June 1, 2026 13:56
@ethernet8023
ethernet8023 marked this pull request as ready for review June 1, 2026 15:50
@ethernet8023
ethernet8023 force-pushed the ethie/lockfile branch 2 times, most recently from 8e5b7ee to e67a335 Compare June 2, 2026 14:13
@ethernet8023
ethernet8023 force-pushed the ethie/lockfile branch 2 times, most recently from fd8b7fb to 11c788c Compare June 2, 2026 21:00
@ethernet8023 ethernet8023 changed the title fix(node/nix): use single workspace lockfile fix(node/nix): consolidate workspace lockfile + update all consumers Jun 2, 2026
@ethernet8023
ethernet8023 force-pushed the ethie/lockfile branch 3 times, most recently from 6db2fea to ff6b3e8 Compare June 2, 2026 21:41
@austinpickett
austinpickett requested a review from Copilot June 2, 2026 21:41
austinpickett
austinpickett previously approved these changes Jun 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR consolidates the repo’s Node.js dependency management into a single root-level npm workspace lockfile and updates downstream consumers (Nix builds/devshell, Docker image build, CI scanners, and the Python CLI) to operate from the workspace root. It also updates Hermes CLI behavior/tests to align with workspace-root install semantics.

Changes:

  • Expand root package.json workspaces to include ui-tui, ui-tui/packages/*, and web, enabling a single root package-lock.json.
  • Refactor Nix npm packaging helpers to share src/npmDeps/hash and centralize devshell lockfile maintenance + fix-lockfiles.
  • Make hermes_cli/main.py workspace-root-aware for npm install/build flows; update Dockerfile/CI/tests to expect only the root lockfile.

Reviewed changes

Copilot reviewed 16 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
package.json Adds additional workspace members so installs can be driven from the repo root.
ui-tui/packages/hermes-ink/package-lock.json Removes per-package lockfile (now covered by root lockfile).
nix/lib.nix Centralizes npm build inputs and introduces shared devshell + fix-lockfiles logic for a single root lockfile.
nix/devShell.nix Switches devshell setup to a single npm hook driven by collected workspace package.json paths.
nix/tui.nix Updates TUI Nix build to run from workspace root and install output paths accordingly.
nix/web.nix Updates web Nix build to cd web/ and run toolchain from workspace root node_modules.
nix/desktop.nix Updates desktop Nix build to rely on shared npm attrs and build from apps/desktop/.
nix/packages.nix Simplifies fix-lockfiles wiring to a single-attr invocation.
hermes_cli/main.py Adds _workspace_root() and updates TUI/Web npm install behavior to run from workspace root.
Dockerfile Removes per-directory lockfile copies and subdirectory npm installs in favor of a single root install.
.github/workflows/osv-scanner.yml Switches OSV scan inputs from ui-tui/package-lock.json to root package-lock.json.
.github/workflows/nix-lockfile-fix.yml Updates triggers/paths for the single lockfile and corrects dashboard→desktop references.
.envrc Updates direnv watched files to align with workspace-root lockfile + all workspace package manifests.
tests/hermes_cli/test_tui_npm_install.py Adds coverage for workspace-root resolution and workspace lockfile layout.
tests/hermes_cli/test_cmd_update.py Updates expectations for npm calls now occurring at workspace root.
tests/hermes_cli/test_web_ui_build.py Updates lockfile fixture location to project root.
tests/tools/test_dockerfile_pid1_reaping.py Updates assertions to reflect root-only lockfile usage in Dockerfile.
Files not reviewed (1)
  • ui-tui/packages/hermes-ink/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nix/lib.nix
Comment on lines +76 to 79
# Normalize trailing newlines on the root lockfile so source and
# npm-deps always match, regardless of what fetchNpmDeps preserves.
sed -i -z 's/\\n*$/\\n/' package-lock.json

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pre-existing on main — the same \n pattern exists in the current nix/lib.nix on origin/main. Not introduced by this PR. That said, you're right that \n inside single quotes is a literal backslash-n, not a newline — GNU sed with -z doesn't interpret \n inside s///. The sed is effectively a no-op (matches literal \n which doesn't exist in the lockfile). The builds still work because fetchNpmDeps and source happen to preserve matching trailing newlines. I'll fix the pre-existing issue in a follow-up rather than scope-creep this PR.

Comment thread nix/lib.nix
Comment on lines 84 to +88
cat > "$TMPDIR/bin/diff" << DIFFWRAP
#!/bin/sh
f1=\$(mktemp) && sed -z 's/\n*$/\n/' "\$1" > "\$f1"
f2=\$(mktemp) && sed -z 's/\n*$/\n/' "\$2" > "\$f2"
${pkgs.diffutils}/bin/diff "\$f1" "\$f2" && rc=0 || rc=\$?
rm -f "\$f1" "\$f2"
exit \$rc
f1=\\$(mktemp) && sed -z 's/\\n*$/\\n/' "\\$1" > "\\$f1"
f2=\\$(mktemp) && sed -z 's/\\n*$/\\n/' "\\$2" > "\\$f2"
${pkgs.diffutils}/bin/diff "\\$f1" "\\$f2" && rc=0 || rc=\\$?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pre-existing on main — the same unquoted heredoc with \$ escaping exists in the current nix/lib.nix on origin/main. Not introduced by this PR. The unquoted heredoc (<< DIFFWRAP instead of << 'DIFFWRAP') means $(mktemp) and $1/$2 expand at write-time rather than runtime. The wrapper still "works" because npmConfigHook's diff happens to succeed by other means. I'll fix the pre-existing issue in a follow-up.

Comment thread nix/lib.nix
Comment on lines 179 to 180
set -uox pipefail
MODE="''${1:---apply}"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

False positive — set -uox pipefail works correctly. Bash parses -o as taking the next argument, so -uox becomes -u -o x and then pipefail is the argument to -o, which IS pipefail. Tested directly: set -uox pipefail sets pipefail=ON and false | true correctly exits 1. Same pattern exists on main and has been working for months.

Comment thread nix/lib.nix
Comment on lines +248 to +250
REPORT="- [\`$LIB_FILE:$HASH_LINE\`]($LIB_URL): \`$OLD_HASH\` → \`$NEW_HASH\` — lockfile: [\`$LOCK_FILE\`]($LOCK_URL)"$'\\n'
else
REPORT="- \`$LIB_FILE:$HASH_LINE\`: \`$OLD_HASH\` → \`$NEW_HASH\`"$'\\n'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not a bug — Nix '' string escaping processes \\n\n, so the derivation output contains $'\n' (single backslash-n inside ANSI-C quoting), which bash expands to a real newline character. This matches the existing pattern on main in mkFixLockfiles and works correctly.

Comment thread hermes_cli/main.py Outdated
Comment on lines +8803 to +8814
# With a single workspace lockfile, one root install covers all
# workspaces (ui-tui, web, apps/desktop). Previously each workspace
# had its own lockfile and was installed separately.
print("→ Updating Node.js dependencies...")
for label, path in paths:
if not (path / "package.json").exists():
continue
extra_args = ["--no-fund", "--no-audit", "--progress=false"]

# Stream npm output (no `--silent`, no `capture_output`) so any
# optional dependency postinstall scripts (e.g. `agent-browser`'s
# Chromium fetch on first install) print progress instead of
# appearing to hang silently for minutes (#18840). The
# `_UpdateOutputStream` wrapper installed by the updater mirrors
# streamed output to ``~/.hermes/logs/update.log`` so nothing is lost.
#
# The repo root install also passes `--workspaces=false` so npm
# does not recursively install every `apps/*` workspace (dashboard,
# desktop, shared) — those are installed/built on demand via
# `_build_web_ui()` and the desktop launchers.
extra_args = ["--no-fund", "--no-audit", "--progress=false"]
if path == PROJECT_ROOT:
extra_args.append("--workspaces=false")

result = _run_npm_install_deterministic(
npm,
path,
extra_args=tuple(extra_args),
capture_output=False,
)
if result.returncode == 0:
print(f" ✓ {label}")
continue

print(f" ⚠ npm install failed in {label}")
result = _run_npm_install_deterministic(
npm,
PROJECT_ROOT,
extra_args=tuple(extra_args),
capture_output=False,
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7d900228e. _update_node_dependencies now runs in two steps:

  1. npm install --workspaces=false (root deps only)
  2. npm install --workspace ui-tui --workspace web (explicitly selected)

Desktop/electron is excluded from hermes update. Desktop deps are installed on demand by the desktop launcher (see _desktop_build_needed).

Comment thread nix/lib.nix
Comment on lines 100 to 103
set -euox pipefail

REPO_ROOT=$(git rev-parse --show-toplevel)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

False positive — same as the set -uox pipefail comment above. set -euox pipefail is parsed as -e -u -o x then pipefail as the -o argument, which correctly sets pipefail. Tested and verified.

OutThisLife
OutThisLife previously approved these changes Jun 2, 2026
ethernet8023 added a commit that referenced this pull request Jun 2, 2026
_update_node_dependencies now runs in two steps:
  1. root install with --workspaces=false (root deps only)
  2. explicit --workspace ui-tui --workspace web

This avoids downloading Electron during `hermes update` for users
who only need the CLI/TUI/web build.  Desktop deps are installed
on demand by the desktop launcher (see _desktop_build_needed).

Addresses Copilot review comment on PR #36171.
Consolidate per-package package-lock.json files into a single root-level
workspace lockfile.  Update all consumers:

- Nix: shared src/npmDeps/npmDepsHash in lib.nix; devshell hook stamps
  package.json paths then runs npm ci from root; individual .nix files
  use mkNpmPassthru attrs instead of per-package fetchNpmDeps.
- Python CLI: new _workspace_root() helper so _tui_need_npm_install,
  _make_tui_argv, _build_web_ui resolve lockfile/node_modules from the
  workspace root.
- Desktop: replace --force-build/mtime heuristic with content-hash build
  stamp (_compute_desktop_content_hash via pathspec).  Remove --force-build
  flag.
- Dockerfile: single root npm install; no per-directory lockfile copies.
- CI: nix-lockfile-fix and osv-scanner reference root package-lock.json;
  apps/dashboard → apps/desktop.
- Tests: new test_tui_npm_install.py; desktop stamp tests in
  test_gui_command.py; updated assertions in test_cmd_update.py,
  test_web_ui_build.py, test_dockerfile_pid1_reaping.py.
- Docs: remove --force-build from desktop flag table.

Deleted: apps/desktop/package-lock.json, ui-tui/package-lock.json,
ui-tui/packages/hermes-ink/package-lock.json, web/package-lock.json.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/nix Nix flake, NixOS module, container packaging P2 Medium — degraded but workaround exists type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants