mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
Merge pull request #149746 from NousResearch/chore/pnpm-switch
chore(js): switch the workspaces from npm to pnpm
This commit is contained in:
304 files changed
+16243
-20998
No files matched your search
+2
-2
@@ -33,7 +33,7 @@ node_modules
|
||||
.build/
|
||||
|
||||
# Built artifacts that are regenerated inside the image. Excluded so local
|
||||
# rebuilds on the developer's machine don't invalidate the npm-install layer
|
||||
# rebuilds on the developer's machine don't invalidate the dependency-install layer
|
||||
# that now depends on the full ui-tui/packages/hermes-ink/ tree being present.
|
||||
ui-tui/dist/
|
||||
ui-tui/packages/hermes-ink/dist/
|
||||
@@ -78,7 +78,7 @@ runtime/
|
||||
|
||||
# Desktop app source (Tauri/Electron); the app is never installed in the container.
|
||||
# apps/shared is the dashboard↔desktop websocket helper and is linked from
|
||||
# web/package.json as a file: workspace dep — keep it in the build context.
|
||||
# web/package.json as a workspace:* dep — keep it in the build context.
|
||||
# apps/desktop is the Webapp renderer's build input (webapp_build stage); its
|
||||
# local build outputs stay out.
|
||||
apps/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
watch_file flake.nix flake.lock hermes pyproject.toml uv.lock
|
||||
watch_file package-lock.json package.json web/package.json ui-tui/package.json website/package.json apps/shared/package.json apps/desktop/package.json ui-tui/packages/hermes-ink/package.json
|
||||
watch_file pnpm-lock.yaml pnpm-workspace.yaml package.json web/package.json ui-tui/package.json website/package.json apps/shared/package.json apps/desktop/package.json ui-tui/packages/hermes-ink/package.json
|
||||
watch_dir nix
|
||||
watch_dir pm
|
||||
|
||||
|
||||
+5
-1
@@ -41,12 +41,16 @@ pyproject.toml @NousResearch/hermes-agent-core
|
||||
package.json @NousResearch/hermes-agent-core
|
||||
uv.lock @NousResearch/hermes-agent-core
|
||||
*package-lock.json @NousResearch/hermes-agent-core
|
||||
/pnpm-lock.yaml @NousResearch/hermes-agent-core
|
||||
/flake.lock @NousResearch/hermes-agent-core
|
||||
/pm/lock.json @NousResearch/hermes-agent-core
|
||||
*Dockerfile @NousResearch/hermes-agent-core
|
||||
|
||||
# The npm supply-chain quarantine (min-release-age) lives here.
|
||||
# A root .npmrc (registry, auth) would apply to every pnpm install.
|
||||
.npmrc @NousResearch/hermes-agent-core
|
||||
# The pnpm workspace's quarantine (minimumReleaseAge), its overrides, and the
|
||||
# dependencies allowed to run build scripts (allowBuilds) live here.
|
||||
/pnpm-workspace.yaml @NousResearch/hermes-agent-core
|
||||
|
||||
# Where PM fetches artifacts from.
|
||||
/pm/artifact-mirror.json @NousResearch/hermes-agent-core
|
||||
|
||||
@@ -55,7 +55,7 @@ outputs:
|
||||
description: Run the PM project-lock check (pyproject.toml / uv.lock changes only).
|
||||
value: ${{ steps.classify.outputs.uv_lock }}
|
||||
npm_lock:
|
||||
description: Post/update the semantic package-lock.json diff PR comment.
|
||||
description: Post/update the semantic JS lockfile (pnpm-lock.yaml / package-lock.json) diff PR comment.
|
||||
value: ${{ steps.classify.outputs.npm_lock }}
|
||||
bootstrap:
|
||||
description: Run the bootstrap installer lane (install.sh sandbox + stamp verification).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Retry a flaky command
|
||||
description: >-
|
||||
Run a shell command, retrying on non-zero exit. For dependency installs
|
||||
(npm ci, uv sync) whose only failures are transient network/toolchain
|
||||
(pnpm install, uv sync) whose only failures are transient network/toolchain
|
||||
flakes — a node-gyp header fetch, a registry blip — so CI self-heals
|
||||
instead of needing a manual re-run. Can also capture stdout as a step
|
||||
output for commands whose result must be consumed by later steps.
|
||||
|
||||
@@ -12,11 +12,11 @@ not resolve a version range, install another setup action, or modify the lock.
|
||||
toolchain: all
|
||||
extras: '[]'
|
||||
test-environment: 'true'
|
||||
- run: python --version && node --version && npm --version
|
||||
- run: python --version && node --version && pnpm --version
|
||||
```
|
||||
|
||||
`toolchain` defaults to `python` (Python and PM's private installer). `node` installs Node and npm;
|
||||
`all` installs both pairs. There are no version overrides. `extras` is a JSON
|
||||
`toolchain` defaults to `python` (Python and PM's private installer). `node` installs Node, npm and
|
||||
pnpm; `all` installs both sets. There are no version overrides. `extras` is a JSON
|
||||
array because GitHub action inputs are strings. Omit it for tools only; `[]`
|
||||
installs the core Python dependencies. Set `test-environment: 'true'` to build an
|
||||
independent interpreter with the `dev` and `test` dependency groups; any `extras`
|
||||
@@ -24,8 +24,11 @@ then select runtime features in that interpreter. PM checks `uv.lock`, installs
|
||||
the requested dependencies, and validates the environment. Only non-test installs
|
||||
publish an application selection. It does not enable plugins.
|
||||
|
||||
Subsequent steps get `python`, `python3`, `node`, `npm` and `npx`
|
||||
Subsequent steps get `python`, `python3`, `node`, `pnpm`, `npm` and `npx`
|
||||
for the selected toolchain on PATH. The pinned npm precedes Node's bundled npm.
|
||||
pnpm installs the workspace; npm serves the standalone npm projects (website/ and
|
||||
the sidecars). `pnpm_config_store_dir` points pnpm's store into the runner's temp
|
||||
dir, on the checkout's drive, so pnpm can hard-link from it on Windows too.
|
||||
On Windows, PM selects the host architecture even if the bootstrap interpreter
|
||||
runs under x64 emulation. A disposable command environment supplies the missing
|
||||
`python3.exe` alias without changing the verified interpreter store.
|
||||
@@ -48,7 +51,7 @@ The official, SHA-pinned `actions/cache` transports three independent caches:
|
||||
| --- | --- | --- |
|
||||
| Tools | PM store and installed facts | Native target, toolchain and PM lock hash |
|
||||
| Python | PM's actual uv download/build cache | Native target, OS version, Python version, prune policy and dependency-file hash |
|
||||
| Node | `npm config get cache` | Runner OS, native architecture and npm dependency-lock hash |
|
||||
| Node | pnpm's store (`pnpm_config_store_dir`), or `npm config get cache` | Runner OS, native architecture, package manager and dependency-lock hash |
|
||||
|
||||
All restores use the exact primary key, without fallback prefixes, matching
|
||||
setup-uv and setup-node's npm behavior. Successful jobs save at teardown;
|
||||
@@ -61,11 +64,13 @@ Dependency caches never contain `node_modules` or virtual environments.
|
||||
Keep an installed-tree cache in the caller if that job needs one.
|
||||
|
||||
`cache`, `cache-python` and `cache-node` independently disable the store, uv,
|
||||
and npm caches. Each defaults to `true`; language-specific caches run only for
|
||||
and node caches. Each defaults to `true`; language-specific caches run only for
|
||||
that toolchain. `python-cache-dependency-glob` defaults to `pyproject.toml` and
|
||||
`uv.lock`. `node-cache-dependency-path` defaults to `package-lock.json`; use
|
||||
`website/package-lock.json` for site jobs. Both accept multiline glob strings.
|
||||
An npm cache without a matching lockfile fails, rather than caching an
|
||||
`uv.lock`. `node-package-manager` selects whose downloads the node cache holds:
|
||||
`pnpm` (default) for the workspace, `npm` for the standalone npm projects.
|
||||
`node-cache-dependency-path` defaults to `pnpm-lock.yaml` and `pnpm-workspace.yaml`;
|
||||
site jobs use `npm` with `website/package-lock.json`. Both accept multiline glob
|
||||
strings. A node cache without a matching lockfile fails, rather than caching an
|
||||
unversioned dependency set.
|
||||
|
||||
`prune-python-cache: true` registers PM's lock-exact cache-pruning operation at teardown,
|
||||
@@ -79,7 +84,7 @@ bundle ship gate enforces) and keeps downloaded wheels the lock still needs —
|
||||
`uv cache prune --ci` would discard them, leaving a snapshot that warms almost
|
||||
nothing.
|
||||
|
||||
Outputs include `python-version`, `uv-version`, `node-version`, `npm-version`,
|
||||
Outputs include `python-version`, `uv-version`, `node-version`, `npm-version`, `pnpm-version`,
|
||||
`python-path`, `venv`, `target`, and the three `*-cache-hit` flags.
|
||||
Use the version outputs in installed-tree cache keys instead of repeating pins.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Set up the locked PM toolchain
|
||||
description: Install Python/uv and Node/npm from pm/lock.json; cache tools and dependency downloads.
|
||||
description: Install Python/uv and Node/npm/pnpm from pm/lock.json; cache tools and dependency downloads.
|
||||
inputs:
|
||||
toolchain:
|
||||
description: python (Python and uv), node (Node and npm), or all.
|
||||
description: python (Python and uv), node (Node, npm and pnpm), or all.
|
||||
default: python
|
||||
packages:
|
||||
description: Comma-separated extra PM tools to install beyond the toolchain roots (e.g. ffmpeg). Rides the tools cache like npm/uv.
|
||||
@@ -32,19 +32,24 @@ inputs:
|
||||
description: Save the uv cache on success. Set false for restore-only jobs.
|
||||
default: ${{ github.event_name == 'pull_request' && 'false' || 'true' }}
|
||||
cache-node:
|
||||
description: Cache npm downloads; never cache node_modules.
|
||||
description: Cache the package manager's downloads; never cache node_modules.
|
||||
default: 'true'
|
||||
save-node-cache:
|
||||
description: Save the npm cache on success. Set false for restore-only jobs.
|
||||
description: Save the node cache on success. Set false for restore-only jobs.
|
||||
default: ${{ github.event_name == 'pull_request' && 'false' || 'true' }}
|
||||
python-cache-dependency-glob:
|
||||
description: Dependency files that invalidate the uv cache.
|
||||
default: |
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
node-package-manager:
|
||||
description: Whose downloads the node cache holds; pnpm (the workspace) or npm (website/ and the other standalone npm projects).
|
||||
default: pnpm
|
||||
node-cache-dependency-path:
|
||||
description: npm lockfiles that invalidate the npm download cache; supports multiline globs.
|
||||
default: package-lock.json
|
||||
description: Files that invalidate the node download cache; supports multiline globs.
|
||||
default: |
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
prune-python-cache:
|
||||
description: Use uv cache prune --ci before automatic saves. Explicit bundle saves preserve downloaded wheels.
|
||||
default: 'false'
|
||||
@@ -64,6 +69,9 @@ outputs:
|
||||
npm-version:
|
||||
description: Locked npm version.
|
||||
value: ${{ steps.prepare.outputs.npm-version }}
|
||||
pnpm-version:
|
||||
description: Locked pnpm version.
|
||||
value: ${{ steps.prepare.outputs.pnpm-version }}
|
||||
python-path:
|
||||
description: Interpreter for subsequent steps, including requested extras.
|
||||
value: ${{ steps.dependencies.outputs.python-path || steps.install.outputs.python-path }}
|
||||
@@ -80,7 +88,7 @@ outputs:
|
||||
description: Exact uv dependency cache hit.
|
||||
value: ${{ steps.python-cache.outputs.cache-hit || steps.python-cache-restore.outputs.cache-hit }}
|
||||
node-cache-hit:
|
||||
description: Exact npm dependency cache hit.
|
||||
description: Exact node dependency cache hit.
|
||||
value: ${{ steps.node-cache.outputs.cache-hit || steps.node-cache-restore.outputs.cache-hit }}
|
||||
runs:
|
||||
using: composite
|
||||
@@ -179,34 +187,36 @@ runs:
|
||||
cache: ${{ steps.install.outputs.uv-cache-path }}
|
||||
lock-source: ${{ github.workspace }}
|
||||
|
||||
- name: Require an npm dependency lock for caching
|
||||
- name: Require a dependency lock for caching
|
||||
if: inputs.toolchain != 'python' && inputs.cache-node == 'true' && hashFiles(inputs.node-cache-dependency-path) == ''
|
||||
shell: bash
|
||||
run: |
|
||||
printf '%s\n' '::error::No npm lock matched node-cache-dependency-path.'
|
||||
printf '%s\n' '::error::No lock matched node-cache-dependency-path.'
|
||||
exit 1
|
||||
|
||||
- name: Cache npm dependency downloads
|
||||
# pnpm's content-addressed store, or npm's download cache for the standalone
|
||||
# npm projects.
|
||||
- name: Cache node dependency downloads
|
||||
id: node-cache
|
||||
if: inputs.toolchain != 'python' && inputs.cache-node == 'true' && inputs.save-node-cache == 'true'
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.install.outputs.npm-cache-path }}
|
||||
key: node-cache-${{ runner.os }}-${{ steps.prepare.outputs.arch }}-npm-${{ hashFiles(inputs.node-cache-dependency-path) }}${{ inputs.cache-suffix != '' && format('-{0}', inputs.cache-suffix) || '' }}
|
||||
path: ${{ inputs.node-package-manager == 'npm' && steps.install.outputs.npm-cache-path || steps.install.outputs.pnpm-store-path }}
|
||||
key: node-cache-${{ runner.os }}-${{ steps.prepare.outputs.arch }}-${{ inputs.node-package-manager }}-${{ hashFiles(inputs.node-cache-dependency-path) }}${{ inputs.cache-suffix != '' && format('-{0}', inputs.cache-suffix) || '' }}
|
||||
# A lockfile bump misses the exact key, but the previous download set
|
||||
# is still mostly valid — npm verifies every tarball it reinstalls.
|
||||
# is still mostly valid — both verify every package they reinstall.
|
||||
restore-keys: |
|
||||
node-cache-${{ runner.os }}-${{ steps.prepare.outputs.arch }}-npm-
|
||||
node-cache-${{ runner.os }}-${{ steps.prepare.outputs.arch }}-${{ inputs.node-package-manager }}-
|
||||
|
||||
- name: Restore npm dependency downloads
|
||||
- name: Restore node dependency downloads
|
||||
id: node-cache-restore
|
||||
if: inputs.toolchain != 'python' && inputs.cache-node == 'true' && inputs.save-node-cache == 'false'
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.install.outputs.npm-cache-path }}
|
||||
key: node-cache-${{ runner.os }}-${{ steps.prepare.outputs.arch }}-npm-${{ hashFiles(inputs.node-cache-dependency-path) }}${{ inputs.cache-suffix != '' && format('-{0}', inputs.cache-suffix) || '' }}
|
||||
path: ${{ inputs.node-package-manager == 'npm' && steps.install.outputs.npm-cache-path || steps.install.outputs.pnpm-store-path }}
|
||||
key: node-cache-${{ runner.os }}-${{ steps.prepare.outputs.arch }}-${{ inputs.node-package-manager }}-${{ hashFiles(inputs.node-cache-dependency-path) }}${{ inputs.cache-suffix != '' && format('-{0}', inputs.cache-suffix) || '' }}
|
||||
restore-keys: |
|
||||
node-cache-${{ runner.os }}-${{ steps.prepare.outputs.arch }}-npm-
|
||||
node-cache-${{ runner.os }}-${{ steps.prepare.outputs.arch }}-${{ inputs.node-package-manager }}-
|
||||
|
||||
- name: Install the requested Python dependencies through PM
|
||||
id: dependencies
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# Deliberately scoped to github-actions only.
|
||||
#
|
||||
# We do NOT enable Dependabot for pip / npm / any source-dependency ecosystem
|
||||
# because we pin source dependencies exactly (uv.lock, package-lock.json) as
|
||||
# part of our supply-chain posture. Automatic version-bump PRs against those
|
||||
# pins would undermine the strategy — pins are moved deliberately, after
|
||||
# review, not on a schedule.
|
||||
# because we pin source dependencies exactly (uv.lock, pnpm-lock.yaml,
|
||||
# package-lock.json) as part of our supply-chain posture. Automatic
|
||||
# version-bump PRs against those pins would undermine the strategy — pins are
|
||||
# moved deliberately, after review, not on a schedule.
|
||||
#
|
||||
# github-actions is the exception: action pins (we use full commit SHAs per
|
||||
# supply-chain policy) must be updated when upstream actions publish
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// plain `check` gives that. This is the same selection rule the old CI matrix
|
||||
// used, so the set of commands is unchanged. Only the schedule is different.
|
||||
//
|
||||
// This is not `npm run --ws check`, because that command is serial and stops
|
||||
// This is not `pnpm -r run check`, because that command stops
|
||||
// at the first workspace that fails. This runs every unit and fails at the
|
||||
// end with the full list.
|
||||
//
|
||||
@@ -17,23 +17,30 @@
|
||||
// `--concurrency N` sets the limit. `--list` prints the units and exits.
|
||||
|
||||
import { execFileSync, spawn } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { availableParallelism } from 'node:os'
|
||||
import { join, relative } from 'node:path'
|
||||
|
||||
const IS_CI = Boolean(process.env.GITHUB_ACTIONS)
|
||||
const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
||||
const PNPM = 'pnpm'
|
||||
|
||||
/** @returns {{pkg: string, script: string}[]} */
|
||||
function discoverUnits() {
|
||||
const raw = execFileSync(NPM, ['query', '.workspace'], {
|
||||
const raw = execFileSync(PNPM, ['ls', '--recursive', '--depth', '-1', '--json'], {
|
||||
encoding: 'utf-8',
|
||||
shell: process.platform === 'win32',
|
||||
})
|
||||
/** @type {{location: string, scripts?: Record<string,string>}[]} */
|
||||
const pkgs = JSON.parse(raw)
|
||||
/** @type {{path: string}[]} */
|
||||
const projects = JSON.parse(raw)
|
||||
const root = process.cwd()
|
||||
|
||||
/** @type {{pkg: string, script: string}[]} */
|
||||
const units = []
|
||||
for (const pkg of pkgs) {
|
||||
for (const project of projects) {
|
||||
const location = relative(root, project.path).split('\\').join('/')
|
||||
// The root's `check` runs every workspace's check: it is not a unit.
|
||||
if (!location) continue
|
||||
const pkg = { location, ...JSON.parse(readFileSync(join(project.path, 'package.json'), 'utf-8')) }
|
||||
const scripts = pkg.scripts || {}
|
||||
const subs = Object.keys(scripts).filter((s) => /^check:.+$/.test(s))
|
||||
if (subs.length > 0) {
|
||||
@@ -49,7 +56,7 @@ function discoverUnits() {
|
||||
function runUnit(unit) {
|
||||
return new Promise((resolve) => {
|
||||
const started = Date.now()
|
||||
const child = spawn(NPM, ['run', '--prefix', unit.pkg, unit.script], {
|
||||
const child = spawn(PNPM, ['--dir', unit.pkg, 'run', unit.script], {
|
||||
// Buffer, and do not inherit. Children that share one stdout
|
||||
// interleave their lines, and a failure is then hard to read.
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
|
||||
@@ -184,7 +184,7 @@ Then a gate step in `ci.yaml` rewrites the result:
|
||||
|---|---|
|
||||
| `python` | anything not provably irrelevant to Python (includes `skills/`, `website/docs/`, `website/scripts/`, `website/static/oauth/`) |
|
||||
| `python_prod` | as `python`, excluding `tests/` |
|
||||
| `frontend` | `ui-tui/`, `web/`, `apps/`, `tests-js/`, root npm manifests, the WhatsApp bridge, JS under `scripts/build/` |
|
||||
| `frontend` | `ui-tui/`, `web/`, `apps/`, `tests-js/`, root pnpm manifests (`package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`), the WhatsApp bridge, JS under `scripts/build/` |
|
||||
| `rust` | `.rs`, Cargo files, bootstrap-installer crate paths |
|
||||
| `catalog` | `plugin-catalog/*.yaml`, `contributors/emails/` (a diff with only these runs catalog tests **instead of** the Python suite) |
|
||||
| `site` | the Docusaurus site |
|
||||
@@ -194,7 +194,7 @@ Then a gate step in `ci.yaml` rewrites the result:
|
||||
| `uv_lock` | `pyproject.toml` or `uv.lock` |
|
||||
| `deps` | `pyproject.toml` |
|
||||
| `scan` | Python files, `.pth`, setup hooks |
|
||||
| `npm_lock` | any `package-lock.json` |
|
||||
| `npm_lock` | `pnpm-lock.yaml` or any `package-lock.json` |
|
||||
| `docker_meta` | Dockerfiles and Docker scripts |
|
||||
| `docker`, `nix` | slow lanes, read by `docker.yml` and `nix.yml` (not by `ci.yaml`) |
|
||||
| `e2e`, `e2e_upgrade`, `e2e_desktop_core`, `e2e_desktop_update` | computed, then forced off except on release or dispatch |
|
||||
@@ -233,7 +233,7 @@ of every suite that reads it.
|
||||
| ↳ Deny unrelated histories (step) | | `pull_request` only | push, dispatch, release |
|
||||
| ↳ Check contributors (step) | | `python` or `catalog` | neither |
|
||||
| ↳ Check uv.lock (step) | | `uv_lock` | neither lockfile nor pyproject changed |
|
||||
| ↳ package-lock.json diff (advisory steps) | | PR and `npm_lock` | push, or no lockfile change |
|
||||
| ↳ JS lockfile diff, `pnpm-lock.yaml` and `package-lock.json` (advisory steps) | | PR and `npm_lock` | push, or no lockfile change |
|
||||
| ↳ Prompt surface diff (advisory steps) | | PR and `python` | push, or no Python change |
|
||||
| ↳ Lint Docker scripts (steps) | | `docker_meta` | no Docker meta change |
|
||||
| ↳ Supply-chain scan / PyPI upper bounds (steps) | | PR and `scan` / `deps` | push, or neither lane |
|
||||
@@ -344,7 +344,7 @@ branch protection.
|
||||
| `skills-index.yml` | 06:00, 18:00 | push, dispatch | See above |
|
||||
| `skills-index-freshness.yml` | every 4 h | dispatch | Upstream only. Opens an issue if `/docs/api/skills-index.json` is more than 26 h stale, missing, or a source collapsed. Also checks CORS on `/docs/api/plugins.json`. |
|
||||
| `docs-search-monitor.yml` | every 6 h at :17 | `workflow_run` after every **Deploy Site**; dispatch | Upstream only. Live Algolia and headless-browser probe of docs search. A red run is the alert. |
|
||||
| `osv-scanner.yml` | Mondays 09:00 | dispatch | OSV scan of `uv.lock` and `package-lock.json`. Results go to the Security tab. Advisory (does not fail on vulnerabilities). Intentionally not per-PR. |
|
||||
| `osv-scanner.yml` | Mondays 09:00 | dispatch | OSV scan of `uv.lock`, `pnpm-lock.yaml` and the nested `package-lock.json` files. Results go to the Security tab. Advisory (does not fail on vulnerabilities). Intentionally not per-PR. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ name: Bootstrap installer build
|
||||
# Azure Trusted Signing, macOS arm64 dmg signed with the Developer ID cert and
|
||||
# notarized. Uploaded as run artifacts only; nothing is published.
|
||||
#
|
||||
# No caches of any kind: no actions/cache, no setup-node cache, and fresh
|
||||
# npm/cargo/electron-builder cache dirs under RUNNER_TEMP.
|
||||
# No caches of any kind: no actions/cache, no setup-pm cache, and fresh
|
||||
# pnpm/cargo/electron-builder cache dirs under RUNNER_TEMP (setup-pm keeps
|
||||
# pnpm's store there too).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -50,9 +51,11 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # 7.0.0
|
||||
- uses: ./.github/actions/setup-pm
|
||||
with:
|
||||
node-version: 26.12.1
|
||||
toolchain: node
|
||||
cache: false
|
||||
cache-node: false
|
||||
|
||||
- name: Fresh cache dirs
|
||||
shell: bash
|
||||
@@ -60,7 +63,7 @@ jobs:
|
||||
{
|
||||
echo "CARGO_HOME=$RUNNER_TEMP/cargo-home"
|
||||
echo "CARGO_TARGET_DIR=$RUNNER_TEMP/target"
|
||||
echo "npm_config_cache=$RUNNER_TEMP/npm-cache"
|
||||
echo "pnpm_config_cache_dir=$RUNNER_TEMP/pnpm-cache"
|
||||
echo "ELECTRON_BUILDER_CACHE=$RUNNER_TEMP/electron-builder-cache"
|
||||
} >> "$GITHUB_ENV"
|
||||
echo "$USERPROFILE/.cargo/bin" >> "$GITHUB_PATH"
|
||||
@@ -87,9 +90,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
rustup target add x86_64-pc-windows-msvc
|
||||
npm ci --workspace apps/bootstrap-installer --include-workspace-root=false --prefer-online --no-audit --no-fund
|
||||
pnpm install --frozen-lockfile --filter "{./apps/bootstrap-installer}..."
|
||||
cd apps/bootstrap-installer
|
||||
npx tauri build --target x86_64-pc-windows-msvc --no-bundle
|
||||
pnpm exec tauri build --target x86_64-pc-windows-msvc --no-bundle
|
||||
exe="$CARGO_TARGET_DIR/x86_64-pc-windows-msvc/release/Hermes-Setup.exe"
|
||||
mkdir -p "$RUNNER_TEMP/out"
|
||||
cp "$exe" "$RUNNER_TEMP/out/Hermes-Setup.exe"
|
||||
@@ -126,7 +129,7 @@ jobs:
|
||||
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
|
||||
AZURE_TOKEN_CREDENTIALS: prod
|
||||
run: |
|
||||
npm ci --workspace apps/desktop --include-workspace-root=false --prefer-online --no-audit --no-fund
|
||||
pnpm install --frozen-lockfile --filter "{./apps/desktop}..."
|
||||
# shellcheck disable=SC2016 # JS template literal, not shell
|
||||
EXE="$(cygpath -w "$RUNNER_TEMP/out/Hermes-Setup.exe")" node --input-type=module -e '
|
||||
import { pathToFileURL } from "node:url"
|
||||
@@ -160,16 +163,18 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # 7.0.0
|
||||
- uses: ./.github/actions/setup-pm
|
||||
with:
|
||||
node-version: 26.12.1
|
||||
toolchain: node
|
||||
cache: false
|
||||
cache-node: false
|
||||
|
||||
- name: Fresh cache dirs
|
||||
run: |
|
||||
{
|
||||
echo "CARGO_HOME=$RUNNER_TEMP/cargo-home"
|
||||
echo "CARGO_TARGET_DIR=$RUNNER_TEMP/target"
|
||||
echo "npm_config_cache=$RUNNER_TEMP/npm-cache"
|
||||
echo "pnpm_config_cache_dir=$RUNNER_TEMP/pnpm-cache"
|
||||
} >> "$GITHUB_ENV"
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
@@ -236,9 +241,9 @@ jobs:
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
npm ci --workspace apps/bootstrap-installer --include-workspace-root=false --prefer-online --no-audit --no-fund
|
||||
pnpm install --frozen-lockfile --filter "{./apps/bootstrap-installer}..."
|
||||
cd apps/bootstrap-installer
|
||||
npx tauri build --target aarch64-apple-darwin --bundles app,dmg
|
||||
pnpm exec tauri build --target aarch64-apple-darwin --bundles app,dmg
|
||||
|
||||
- name: Sign, notarize and staple the dmg
|
||||
run: |
|
||||
|
||||
@@ -293,7 +293,7 @@ jobs:
|
||||
|
||||
# Every cheap repository guard (infographics, profile archives, case collisions, large blobs,
|
||||
# lazy_deps imports, stale icons, unrelated histories, contributor attribution, uv.lock,
|
||||
# prompt-surface and package-lock diffs, supply-chain scan, Docker lint) as steps of ONE job.
|
||||
# prompt-surface and JS lockfile diffs, supply-chain scan, Docker lint) as steps of ONE job.
|
||||
# Each used to be its own job on the same runner: a full billed minute for 0-13s of work.
|
||||
# Each step keeps its own lane gate, so the detect outputs it needs are passed down.
|
||||
repo-guards:
|
||||
|
||||
@@ -74,6 +74,7 @@ jobs:
|
||||
with:
|
||||
toolchain: all
|
||||
extras: '[]'
|
||||
node-package-manager: npm
|
||||
node-cache-dependency-path: website/package-lock.json
|
||||
|
||||
- name: Install ruamel.yaml for skill extraction
|
||||
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
- name: Install locked chat driver dependencies
|
||||
id: dependencies
|
||||
shell: bash
|
||||
run: npm ci --workspace tests-js --include-workspace-root --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
run: pnpm install --frozen-lockfile --filter "{./tests-js}..." --prod --ignore-scripts
|
||||
|
||||
- name: Fetch one exact downloadable artifact
|
||||
id: artifact
|
||||
@@ -249,7 +249,7 @@ jobs:
|
||||
# before Playwright attaches (every x64 smoke there failed; the same commit's arm64 MSIX on a
|
||||
# GitHub-hosted runner chats), as the install/update GUI legs did (f1b290e7734).
|
||||
runs-on: ${{ inputs.arch == 'arm64' && 'windows-latest-32-arm-core' || 'windows-latest-32-core' }}
|
||||
# windows-11-arm runners are much slower (npm ci alone runs 12+ minutes and
|
||||
# windows-11-arm runners are much slower (the dependency install alone runs 12+ minutes and
|
||||
# the MSIX leg exceeds 30 total); x64 legs complete in ~14.
|
||||
timeout-minutes: 45
|
||||
cache-mode: read
|
||||
|
||||
@@ -489,8 +489,7 @@ jobs:
|
||||
run: |
|
||||
$paths = @(
|
||||
$env:GITHUB_WORKSPACE,
|
||||
$env:RUNNER_TEMP,
|
||||
(npm config get cache)
|
||||
$env:RUNNER_TEMP
|
||||
)
|
||||
foreach ($p in $paths) {
|
||||
try {
|
||||
@@ -571,7 +570,7 @@ jobs:
|
||||
# Universal-bundle assembly reuses two small slices of this leg's preparation
|
||||
# rather than the multi-GB dependency snapshot: electron-builder's toolset
|
||||
# downloads (winCodeSign kit, ATS dlib, dotnet runtime; preparation points
|
||||
# ELECTRON_BUILDER_CACHE at packager/builder) and npm's content cache.
|
||||
# ELECTRON_BUILDER_CACHE at packager/builder) and pnpm's store.
|
||||
# Commit builds hold a read-only cache token, so their save is skipped here.
|
||||
- name: Save the bundle-assembly caches
|
||||
if: ${{ !cancelled() && steps.prepare.outcome == 'success' && matrix.target.label == 'win32-x64' && inputs.build_commit == '' && inputs.channel == '' }}
|
||||
@@ -579,8 +578,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.cache/desktop-inputs/packager/builder
|
||||
${{ github.workspace }}/.cache/desktop-inputs/npm/_cacache
|
||||
key: win32-assembly-v1-${{ runner.arch }}-${{ hashFiles('package-lock.json') }}
|
||||
${{ github.workspace }}/.cache/desktop-inputs/pnpm/store
|
||||
key: win32-assembly-v1-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }}
|
||||
|
||||
- name: Restore verified payload signatures
|
||||
id: payload-signatures
|
||||
@@ -670,7 +669,7 @@ jobs:
|
||||
if: inputs.skip-tests != true
|
||||
shell: bash
|
||||
working-directory: apps/desktop
|
||||
run: node ../../node_modules/vitest/vitest.mjs run --project electron scripts/payload-sign-cache.test.mjs scripts/batch-sign-binaries.test.mjs
|
||||
run: node node_modules/vitest/vitest.mjs run --project electron scripts/payload-sign-cache.test.mjs scripts/batch-sign-binaries.test.mjs
|
||||
|
||||
# Commit jobs share these steps but have read-only cache tokens.
|
||||
- name: Save verified payload signatures
|
||||
@@ -1394,7 +1393,6 @@ jobs:
|
||||
HERMES_PAYLOAD_VERSION: ${{ needs.validate.outputs.payload-version }}
|
||||
# The paths build-win32-x64 saves its bundle-assembly caches from.
|
||||
ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/desktop-inputs/packager/builder
|
||||
npm_config_cache: ${{ github.workspace }}/.cache/desktop-inputs/npm
|
||||
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||
CLOUDFLARE_R2_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||
CLOUDFLARE_R2_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||
@@ -1434,8 +1432,8 @@ jobs:
|
||||
archive-inputs: ${{ inputs.disposable_run == '' && inputs.disposable_channel == '' }}
|
||||
cache-python: false
|
||||
save-tools-cache: false
|
||||
# npm's cache comes from build-win32-x64 below; nothing ever saves
|
||||
# setup-pm's per-job npm key for this read-only job.
|
||||
# pnpm's store comes from build-win32-x64 below; nothing ever saves
|
||||
# setup-pm's per-job node key for this read-only job.
|
||||
cache-node: false
|
||||
|
||||
- name: Restore the bundle-assembly caches
|
||||
@@ -1443,15 +1441,19 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.cache/desktop-inputs/packager/builder
|
||||
${{ github.workspace }}/.cache/desktop-inputs/npm/_cacache
|
||||
key: win32-assembly-v1-${{ runner.arch }}-${{ hashFiles('package-lock.json') }}
|
||||
${{ github.workspace }}/.cache/desktop-inputs/pnpm/store
|
||||
key: win32-assembly-v1-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }}
|
||||
restore-keys: win32-assembly-v1-${{ runner.arch }}-
|
||||
|
||||
- name: Install the locked Windows bundle tooling
|
||||
uses: ./.github/actions/retry
|
||||
env:
|
||||
# The store restored above. A step env, because setup-pm's
|
||||
# GITHUB_ENV export would override a job-level one.
|
||||
pnpm_config_store_dir: ${{ github.workspace }}/.cache/desktop-inputs/pnpm/store
|
||||
with:
|
||||
# No Electron/native postinstalls: only the builder's SDK downloader.
|
||||
command: npm ci --workspace apps/desktop --include-workspace-root --include=dev --ignore-scripts --no-audit --no-fund
|
||||
command: pnpm install --frozen-lockfile --filter "{./apps/desktop}..." --ignore-scripts
|
||||
|
||||
- name: Retrieve Windows packages from R2
|
||||
shell: bash
|
||||
@@ -1792,6 +1794,8 @@ jobs:
|
||||
uses: ./.github/actions/setup-pm
|
||||
with:
|
||||
toolchain: python
|
||||
# scripts/termux/build.py installs and builds the TUI on this host with pnpm.
|
||||
packages: pnpm
|
||||
archive-inputs: true
|
||||
cache-python: false
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-pm
|
||||
with:
|
||||
toolchain: all
|
||||
node-package-manager: npm
|
||||
node-cache-dependency-path: website/package-lock.json
|
||||
|
||||
- name: Install website dependencies
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
# explicit binary preparation before the native fixtures launch.
|
||||
- uses: ./.github/actions/retry
|
||||
with:
|
||||
command: npm ci
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Prepare locked Electron runtime
|
||||
run: node scripts/build/electron-runtime.mjs --source .
|
||||
@@ -65,18 +65,18 @@ jobs:
|
||||
- name: Build the desktop app
|
||||
working-directory: apps/desktop
|
||||
# The generator uses the runtime's Pillow + resvg-py, not a build-only venv.
|
||||
run: npm run build
|
||||
run: pnpm run build
|
||||
|
||||
# webapp-gateway-attach.spec.ts drives the Webapp from a real browser: it needs the
|
||||
# Webapp renderer and a Chromium Playwright can launch.
|
||||
- name: Build the Webapp renderer
|
||||
working-directory: apps/desktop
|
||||
run: npm run build:webapp
|
||||
run: pnpm run build:webapp
|
||||
|
||||
- uses: ./.github/actions/retry
|
||||
with:
|
||||
working-directory: apps/desktop
|
||||
command: npx playwright install --with-deps chromium
|
||||
command: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
# Package the Linux app the way users get it (electron-builder --dir; no
|
||||
# installer formats, no publish). The packaging preparer acquires its
|
||||
@@ -84,13 +84,13 @@ jobs:
|
||||
# on this output (#121097) and boots the packaged binary to a first chat.
|
||||
- name: Package the desktop app (electron-builder --dir)
|
||||
working-directory: apps/desktop
|
||||
run: npm run builder -- --dir --publish never --linux
|
||||
run: pnpm run builder --dir --publish never --linux
|
||||
|
||||
- name: Run the core suite under xvfb
|
||||
working-directory: apps/desktop
|
||||
run: |
|
||||
xvfb-run -a --server-args="-screen 0 1280x1024x24" \
|
||||
npx playwright test -c e2e/core/playwright.config.ts
|
||||
pnpm exec playwright test -c e2e/core/playwright.config.ts
|
||||
env:
|
||||
CI: 'true'
|
||||
HERMES_E2E_PYTHON: ${{ steps.pm.outputs.python-path }}
|
||||
|
||||
@@ -76,10 +76,10 @@ jobs:
|
||||
test-environment: 'true'
|
||||
|
||||
# The Playwright runner and its Electron driver. The install under test
|
||||
# runs its own npm install inside the sandbox.
|
||||
# runs its own dependency install inside the sandbox.
|
||||
- uses: ./.github/actions/retry
|
||||
with:
|
||||
command: npm ci
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run the update suite under xvfb
|
||||
working-directory: apps/desktop
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
echo "::endgroup::"
|
||||
done ) &
|
||||
xvfb-run -a --server-args="-screen 0 1280x1024x24" \
|
||||
npx playwright test -c e2e/update/playwright.config.ts ${{ matrix.specs }}
|
||||
pnpm exec playwright test -c e2e/update/playwright.config.ts ${{ matrix.specs }}
|
||||
env:
|
||||
CI: 'true'
|
||||
HERMES_E2E_PYTHON: ${{ steps.pm.outputs.python-path }}
|
||||
|
||||
@@ -55,18 +55,18 @@ jobs:
|
||||
test-environment: 'true'
|
||||
prune-python-cache: true
|
||||
|
||||
# Full npm ci (not --ignore-scripts): native addons need lifecycle scripts;
|
||||
# Full install (not --ignore-scripts): native addons need lifecycle scripts;
|
||||
# Electron's binary is prepared explicitly below.
|
||||
# node-pty's native build is needed for the terminal pane.
|
||||
- uses: ./.github/actions/retry
|
||||
with:
|
||||
command: npm ci
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Prepare locked Electron runtime
|
||||
run: node scripts/build/electron-runtime.mjs --source .
|
||||
|
||||
# ── Build desktop app ─────────────────────────────────────────────
|
||||
# The Playwright step below runs `npm run build` before testing so
|
||||
# The Playwright step below runs `pnpm run build` before testing so
|
||||
# dist/ is always fresh — no separate build step needed here.
|
||||
|
||||
# ── Restore visual baseline screenshots from main ──────────────────
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
# xvfb runs at a fixed 1280x1024 screen so the 1220x800 Electron
|
||||
# window always has a consistent viewport for screenshot comparison.
|
||||
# On main, we run with --update-snapshots to generate baselines.
|
||||
# `npm run test:e2e` builds dist/ as a pretest hook so the renderer
|
||||
# `pnpm run test:e2e` builds dist/ as a pretest hook so the renderer
|
||||
# is always fresh — no separate build step needed.
|
||||
- name: Run Playwright E2E tests
|
||||
working-directory: apps/desktop
|
||||
@@ -96,12 +96,12 @@ jobs:
|
||||
# A stable run is on main too, but it tests a candidate. It compares and never rewrites baselines.
|
||||
if [ "${{ github.ref_name }}" = "main" ] && [ -z "${{ inputs.commit }}" ]; then
|
||||
echo "On main — generating/updating baseline screenshots"
|
||||
npm run build && xvfb-run -a --server-args="-screen 0 1280x1024x24" \
|
||||
npx playwright test --reporter=list --update-snapshots
|
||||
pnpm run build && xvfb-run -a --server-args="-screen 0 1280x1024x24" \
|
||||
pnpm exec playwright test --reporter=list --update-snapshots
|
||||
else
|
||||
echo "On PR — comparing against cached baselines"
|
||||
npm run build && xvfb-run -a --server-args="-screen 0 1280x1024x24" \
|
||||
npx playwright test --reporter=list
|
||||
pnpm run build && xvfb-run -a --server-args="-screen 0 1280x1024x24" \
|
||||
pnpm exec playwright test --reporter=list
|
||||
fi
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
echo "- [visual-diffs]($DIFFS_URL) — just the diffed screenshots (small, fast to review)"
|
||||
fi
|
||||
echo ""
|
||||
echo "**To update baselines:** merge to main (baselines auto-update on main runs) or run \`npx playwright test --update-snapshots\` locally."
|
||||
echo "**To update baselines:** merge to main (baselines auto-update on main runs) or run \`pnpm exec playwright test --update-snapshots\` locally."
|
||||
|
||||
# Also parse the JSON report for pass/fail counts
|
||||
if [ -f playwright-report/results.json ]; then
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Install locked chat driver dependencies
|
||||
shell: bash
|
||||
run: npm ci --workspace tests-js --include-workspace-root --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
run: pnpm install --frozen-lockfile --filter "{./tests-js}..." --prod --ignore-scripts
|
||||
|
||||
- name: Start screen recording
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
|
||||
- name: Install locked chat driver dependencies
|
||||
shell: bash
|
||||
run: npm ci --workspace tests-js --include-workspace-root --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
run: pnpm install --frozen-lockfile --filter "{./tests-js}..." --prod --ignore-scripts
|
||||
|
||||
- name: Start screen recording
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
|
||||
- name: Install locked chat driver dependencies
|
||||
shell: bash
|
||||
run: npm ci --workspace tests-js --include-workspace-root --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
run: pnpm install --frozen-lockfile --filter "{./tests-js}..." --prod --ignore-scripts
|
||||
|
||||
# One recording mechanism on every OS (Xvfb gives headless linux a
|
||||
# display; the same display serves any app the driver launches).
|
||||
|
||||
@@ -86,8 +86,8 @@ on:
|
||||
required: false
|
||||
type: number
|
||||
# Green legs take up to ~40 min; from v2026.9.24 the update alone runs the venv->PM
|
||||
# takeover plus npm ci and a Desktop rebuild (24 min in 108542725043, 11 of them one
|
||||
# npm ci), and the app-update driver may wait 30 min. pty-run.py's deadline and the
|
||||
# takeover plus the JS install and a Desktop rebuild (24 min in 108542725043, 11 of them
|
||||
# one npm ci), and the app-update driver may wait 30 min. pty-run.py's deadline and the
|
||||
# per-step budgets bound real hangs, so the job cap only has to clear a slow runner.
|
||||
default: 90
|
||||
bundle-manifest-url:
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
cache-node: false
|
||||
- name: Install locked chat driver dependencies
|
||||
shell: bash
|
||||
run: npm ci --workspace tests-js --include-workspace-root --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
run: pnpm install --frozen-lockfile --filter "{./tests-js}..." --prod --ignore-scripts
|
||||
- name: Verify native helpers
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -217,7 +217,7 @@ jobs:
|
||||
|
||||
- name: Install locked chat driver dependencies
|
||||
shell: bash
|
||||
run: npm ci --workspace tests-js --include-workspace-root --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
run: pnpm install --frozen-lockfile --filter "{./tests-js}..." --prod --ignore-scripts
|
||||
|
||||
# A released tag prints its one-shot chat through prompt_toolkit, whose Windows
|
||||
# output object needs a console screen buffer -- piping the CLI's stdout into the
|
||||
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
BUNDLE_MACOS_MANIFEST: ${{ inputs.macos-bundle-manifest }}
|
||||
BUNDLE_ROUTE: ${{ inputs.route }}
|
||||
run: |
|
||||
npm ci --ignore-scripts --no-audit --no-fund
|
||||
pnpm install --frozen-lockfile --ignore-scripts
|
||||
node tests/install/e2e-assets/bundle-plan.mjs
|
||||
|
||||
windows-bundled:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: auto-fix lint issues & formatting
|
||||
|
||||
# On push to main (or manual trigger), run `npm run fix` on each workspace
|
||||
# On push to main (or manual trigger), run `pnpm run fix` on each workspace
|
||||
# package and apply any changes via a PR.
|
||||
#
|
||||
# Fixable lint issues (import sorting, unused imports, curly braces, etc.) are
|
||||
@@ -27,7 +27,7 @@ name: auto-fix lint issues & formatting
|
||||
# 2. apply-patch (privileged, contents: write + pull-requests: write)
|
||||
# Checks out, downloads the patch artifact, applies it, pushes to the
|
||||
# bot/js-autofix branch, creates/updates a PR, and enables auto-merge.
|
||||
# This job never runs npm, never installs anything, never executes any
|
||||
# This job never runs pnpm, never installs anything, never executes any
|
||||
# repo code. The only input it trusts is the patch artifact.
|
||||
# Skipped entirely when generate-patch reports no fixes (has-fixes != true).
|
||||
# The PR auto-merges (squash) once CI passes. If CI fails or main moves,
|
||||
@@ -44,7 +44,7 @@ on:
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
steps:
|
||||
- uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1
|
||||
|
||||
- name: Set up locked Node and npm
|
||||
- name: Set up locked Node and pnpm
|
||||
id: node
|
||||
uses: ./.github/actions/setup-pm
|
||||
with:
|
||||
@@ -74,14 +74,14 @@ jobs:
|
||||
# --ignore-scripts: eslint only needs TS sources + eslint packages.
|
||||
- uses: ./.github/actions/retry
|
||||
with:
|
||||
command: npm ci --ignore-scripts
|
||||
command: pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: npm run fix in all workspaces
|
||||
- name: pnpm run fix in all workspaces
|
||||
# continue-on-error: if un-fixable errors exist on main, we still want
|
||||
# to commit whatever fixes were applied. The PR-time check in
|
||||
# typecheck.yml is what blocks un-fixable errors from landing.
|
||||
continue-on-error: true
|
||||
run: npm run fix
|
||||
run: pnpm run fix
|
||||
|
||||
- name: Produce patch
|
||||
id: produce-patch
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
echo "Patch size: $(wc -c < js-fix.patch) bytes"
|
||||
|
||||
# Reject patches that touch anything outside JS/TS/JSON sources.
|
||||
# `npm run fix` should only ever modify those; anything else means
|
||||
# `pnpm run fix` should only ever modify those; anything else means
|
||||
# eslint/prettier or a plugin went rogue and we refuse to ship it.
|
||||
BAD=$(git diff --name-only -- . "${EXCLUDES[@]}" | grep -vE '\.(js|cjs|mjs|ts|tsx|json)$' || true)
|
||||
if [ -n "$BAD" ]; then
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
git commit -m "fmt(js): \`npm run fix\` on merge"
|
||||
git commit -m "fmt(js): \`pnpm run fix\` on merge"
|
||||
|
||||
# Push to the dedicated bot branch. Force-push is safe here:
|
||||
# bot/js-autofix is a bot-only branch that gets rewritten each run.
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
# (https://github.com/<org>/<repo>/pull/<number>).
|
||||
PR_URL=$(gh pr create \
|
||||
--head "$BOT_BRANCH" --base main \
|
||||
--title 'fmt(js): `npm run fix` auto-fix' \
|
||||
--title 'fmt(js): `pnpm run fix` auto-fix' \
|
||||
--body 'Auto-generated by the `auto-fix lint issues & formatting` workflow. Auto-merges (squash) once CI passes. If CI fails or `main` moves, the PR is auto-closed and the branch deleted — the next run re-applies on the current state.')
|
||||
PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$')
|
||||
fi
|
||||
|
||||
@@ -43,11 +43,11 @@ jobs:
|
||||
libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 \
|
||||
xdg-utils libatspi2.0-0 libdrm2 libgbm1 libasound2t64
|
||||
|
||||
# setup-pm caches npm downloads, not the installed workspace tree.
|
||||
# The job then extracts the full workspace node_modules
|
||||
# setup-pm caches pnpm's store, not the installed workspace tree.
|
||||
# The job then links the full workspace node_modules
|
||||
# again and runs the native postinstalls again. Electron's binary is
|
||||
# prepared explicitly below. This caches the installed tree itself, keyed on the
|
||||
# lockfile, and skips ``npm ci`` on an exact hit. There are no
|
||||
# lockfile, and skips ``pnpm install`` on an exact hit. There are no
|
||||
# restore-keys: a partial hit leaves a stale tree, so anything other
|
||||
# than an exact lockfile match reinstalls from the start.
|
||||
#
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
# artifacts and the explicitly prepared Electron dist, both inside the cached tree.
|
||||
#
|
||||
# The ~/.cache/electron download cache stays out of the key on purpose.
|
||||
# ``npm ci`` is skipped on a hit, so nothing reads that cache. It only
|
||||
# ``pnpm install`` is skipped on a hit, so nothing reads that cache. It only
|
||||
# makes the archive larger.
|
||||
- name: Restore node_modules
|
||||
id: node-modules-cache
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
ui-tui/packages/*/node_modules
|
||||
tests-js/node_modules
|
||||
web/node_modules
|
||||
key: node-modules-scripts-${{ runner.os }}-${{ runner.arch }}-node${{ steps.node.outputs.node-version }}-npm${{ steps.node.outputs.npm-version }}-${{ hashFiles('package-lock.json') }}
|
||||
key: node-modules-scripts-${{ runner.os }}-${{ runner.arch }}-node${{ steps.node.outputs.node-version }}-pnpm${{ steps.node.outputs.pnpm-version }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }}
|
||||
|
||||
# Vitest records each file's duration and result under
|
||||
# node_modules/.vite/vitest and orders the next run from it, longest first.
|
||||
@@ -79,10 +79,20 @@ jobs:
|
||||
- name: Drop vitest run history from the restored tree
|
||||
run: rm -rf node_modules/.vite/vitest apps/*/node_modules/.vite/vitest ui-tui/node_modules/.vite/vitest ui-tui/packages/*/node_modules/.vite/vitest tests-js/node_modules/.vite/vitest web/node_modules/.vite/vitest
|
||||
|
||||
# No --trust-lockfile: this install is the gate that checks every locked
|
||||
# version against pnpm-workspace.yaml's release-age policy.
|
||||
- uses: ./.github/actions/retry
|
||||
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||
with:
|
||||
command: npm ci
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
# The cached tree is keyed on the lockfile alone, so an exact hit proves
|
||||
# nothing about the manifests. This checks the lockfile still matches
|
||||
# every package.json and passes the release-age policy, without
|
||||
# touching node_modules.
|
||||
- name: Check the lockfile against the manifests and policy
|
||||
if: steps.node-modules-cache.outputs.cache-hit == 'true'
|
||||
run: pnpm install --frozen-lockfile --lockfile-only
|
||||
|
||||
- name: Prepare locked Electron runtime before parallel checks
|
||||
run: node scripts/build/electron-runtime.mjs --source .
|
||||
@@ -99,9 +109,9 @@ jobs:
|
||||
run: node --test scripts/whatsapp-bridge/*.test.mjs
|
||||
|
||||
# Every check runs at the same time. The step fails only after all of
|
||||
# them finish. There are two reasons this is not ``npm run --ws check``.
|
||||
# them finish. There are two reasons this is not ``pnpm -r run check``.
|
||||
#
|
||||
# * ``--ws`` is serial and stops at the first workspace that fails. A
|
||||
# * ``-r`` stops at the first workspace that fails. A
|
||||
# run then reports one failure, where the matrix this replaced
|
||||
# reported every failure together.
|
||||
# * The unit of work is a CHECK, and not a workspace. apps/desktop is
|
||||
@@ -111,7 +121,7 @@ jobs:
|
||||
# its lint, ui, electron and plugin suites all run together. That
|
||||
# is the same selection rule the old matrix job used.
|
||||
#
|
||||
# Discovery is ``npm query .workspace``. A new package or a new
|
||||
# Discovery is ``pnpm ls -r --depth -1 --json``. A new package or a new
|
||||
# ``check:*`` script needs no change here. An empty list is an error and
|
||||
# not an empty run, because an empty run reports green after it checks
|
||||
# nothing.
|
||||
|
||||
@@ -73,6 +73,33 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
pnpm-deps-hash:
|
||||
name: pnpm deps hash
|
||||
needs: [detect]
|
||||
if: needs.detect.outputs.nix == 'true' || inputs.release == true
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1
|
||||
with:
|
||||
ref: ${{ inputs.commit || github.sha }}
|
||||
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
with:
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
|
||||
# fetchPnpmDeps is a fixed-output derivation: a pnpm-lock.yaml change needs a
|
||||
# new nix/pnpm-deps-hash. flake-check fails too, with the hash buried in its
|
||||
# log; this job fails fast and names the hash Nix computed.
|
||||
- name: Check nix/pnpm-deps-hash
|
||||
run: |
|
||||
set -o pipefail
|
||||
if nix build .#pnpm-deps --no-link --print-build-logs 2>&1 | tee "$RUNNER_TEMP/pnpm-deps.log"; then exit 0; fi
|
||||
got=$(sed -n 's/^ *got: *\(sha256-[A-Za-z0-9+/=]*\)$/\1/p' "$RUNNER_TEMP/pnpm-deps.log")
|
||||
[ -n "$got" ] && echo "::error file=nix/pnpm-deps-hash::pnpm-lock.yaml changed the pnpm store. Set nix/pnpm-deps-hash to $got (or run: nix run .#update-pnpm-lockfile)"
|
||||
exit 1
|
||||
|
||||
flake-check:
|
||||
name: nix flake check
|
||||
needs: [detect]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: OSV-Scanner
|
||||
|
||||
# Scans lockfiles (uv.lock, package-lock.json) against the OSV vulnerability
|
||||
# Scans lockfiles (uv.lock, pnpm-lock.yaml, package-lock.json) against the OSV vulnerability
|
||||
# database. Runs on a weekly schedule against main (and on manual dispatch);
|
||||
# it is deliberately NOT part of per-PR CI — the findings are the repo-wide
|
||||
# baseline of pinned-dep CVEs, identical for every PR, and belong in the
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
# the five sources of truth and skip vendored / test / worktree dirs.
|
||||
scan-args: |-
|
||||
--lockfile=uv.lock
|
||||
--lockfile=package-lock.json
|
||||
--lockfile=pnpm-lock.yaml
|
||||
--lockfile=website/package-lock.json
|
||||
--lockfile=plugins/platforms/photon/sidecar/package-lock.json
|
||||
--lockfile=scripts/whatsapp-bridge/package-lock.json
|
||||
|
||||
@@ -47,7 +47,7 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
npm_lock:
|
||||
description: detect's `npm_lock` lane (package-lock.json semantic diff, PRs only).
|
||||
description: detect's `npm_lock` lane (JS lockfile semantic diff, PRs only).
|
||||
type: boolean
|
||||
default: false
|
||||
uv_lock:
|
||||
@@ -366,11 +366,12 @@ jobs:
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ── Pull requests, npm_lock lane: package-lock.json semantic diff (advisory) ──────────
|
||||
# ── Pull requests, npm_lock lane: JS lockfile semantic diff (advisory) ───────────────
|
||||
|
||||
# The raw textual diff of a lockfile is unreadable (npm reorders entries and rewrites
|
||||
# integrity hashes), so scripts/ci/lockfile_diff.py set-diffs the {install path: version}
|
||||
# maps at the merge base and at HEAD. Review signal, not enforcement.
|
||||
# Covers pnpm-lock.yaml and the standalone npm projects' package-lock.json. The raw
|
||||
# textual diff of a lockfile is unreadable (entries reorder and integrity hashes churn),
|
||||
# so scripts/ci/lockfile_diff.py set-diffs the {package: version} maps at the merge base
|
||||
# and at HEAD. Review signal, not enforcement.
|
||||
- name: Generate semantic lockfile diff
|
||||
id: lockfile-diff
|
||||
if: ${{ !cancelled() && inputs.event_name == 'pull_request' && inputs.npm_lock }}
|
||||
@@ -388,7 +389,7 @@ jobs:
|
||||
if [ -s "$RUNNER_TEMP/lockfile-diff.md" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "## package-lock.json semantic diff"
|
||||
echo "## JS lockfile semantic diff"
|
||||
echo ""
|
||||
cat "$RUNNER_TEMP/lockfile-diff.md"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -409,7 +410,7 @@ jobs:
|
||||
|
||||
if [ "$CHANGED" = "true" ]; then
|
||||
CONTENT=$(cat "$RUNNER_TEMP/lockfile-diff.md" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
||||
STATUS="[{\"source\":\"lockfile-diff\",\"results\":[{\"kind\":\"action_required\",\"title\":\"package-lock.json\",\"summary\":\"Locked npm dependency versions changed.\",\"detail\":${CONTENT},\"how_to_fix\":\"Verify the version changes are expected. package-lock.json changes need a hermes-agent-core review.\"}]}]"
|
||||
STATUS="[{\"source\":\"lockfile-diff\",\"results\":[{\"kind\":\"action_required\",\"title\":\"JS lockfiles\",\"summary\":\"Locked JS dependency versions changed.\",\"detail\":${CONTENT},\"how_to_fix\":\"Verify the version changes are expected. Lockfile changes need a hermes-agent-core review.\"}]}]"
|
||||
else
|
||||
STATUS="[]"
|
||||
fi
|
||||
@@ -460,7 +461,7 @@ jobs:
|
||||
# Three-point diff (base...head) diffs from the merge base to HEAD,
|
||||
# so only changes introduced by this PR are included — not changes
|
||||
# that landed on main after the PR branched off.
|
||||
DIFF=$(git diff "$BASE"..."$HEAD" -- . ':!uv.lock' ':!*.lock' ':!package-lock.json' ':!yarn.lock' || true)
|
||||
DIFF=$(git diff "$BASE"..."$HEAD" -- . ':!uv.lock' ':!*.lock' ':!pnpm-lock.yaml' ':!yarn.lock' || true)
|
||||
|
||||
FINDINGS=""
|
||||
|
||||
|
||||
@@ -86,13 +86,15 @@ jobs:
|
||||
# tests-js generates real icons; the wrapper needs core Pillow/resvg.
|
||||
extras: '[]'
|
||||
cache-node: false
|
||||
# tests-js builds desktop fixtures with apps/desktop's own toolchain (vite,
|
||||
# node-pty): pnpm installs only the selected workspaces, so select it too.
|
||||
- name: Install the locked JS workspace
|
||||
run: npm ci --workspace ui-tui --workspace tests-js --include-workspace-root --include=dev --no-fund --no-audit
|
||||
run: pnpm install --frozen-lockfile --filter "{./ui-tui}..." --filter "{./tests-js}..." --filter "{./apps/desktop}..."
|
||||
- name: Check and bundle the TUI
|
||||
run: |
|
||||
npm run check --workspace tests-js
|
||||
npm run check --workspace ui-tui
|
||||
npm run build --workspace ui-tui
|
||||
pnpm --dir tests-js run check
|
||||
pnpm --dir ui-tui run check
|
||||
pnpm --dir ui-tui run build
|
||||
|
||||
# A job-level `if` cannot read `secrets`, so the gate is a job that exports
|
||||
# whether the release-signing environment is provisioned. Without R2 the
|
||||
|
||||
@@ -122,7 +122,9 @@ jobs:
|
||||
# Installer stage tests inherit setup-pm's HERMES_RUNTIME_DIR. Prepare
|
||||
# the verified Git pin once, before per-file parallelism; otherwise
|
||||
# each isolated PowerShell stage races to download/extract the archive.
|
||||
packages: ${{ matrix.marker == 'windows' && 'git,ripgrep' || 'ripgrep' }}
|
||||
# pnpm (and with it PM's Node): tests that drive the real
|
||||
# scripts/build/node-deps.mjs and source builds need it on PATH.
|
||||
packages: ${{ matrix.marker == 'windows' && 'git,ripgrep,pnpm' || 'ripgrep,pnpm' }}
|
||||
# keychain: the vault-key tests run against the host's real Keychain / Credential Manager.
|
||||
extras: '["all", "telegram", "anthropic", "mistral", "fal", "modal", "daytona", "parallel-web", "keychain"]'
|
||||
test-environment: 'true'
|
||||
|
||||
@@ -72,6 +72,9 @@ jobs:
|
||||
- name: Set up locked Python and test dependencies
|
||||
uses: ./.github/actions/setup-pm
|
||||
with:
|
||||
# Tests that drive the real scripts/build/node-deps.mjs need pnpm (and
|
||||
# with it PM's Node) on PATH.
|
||||
packages: pnpm
|
||||
extras: '["all", "anthropic", "bedrock", "mistral", "fal", "modal", "daytona", "parallel-web", "keychain"]'
|
||||
test-environment: 'true'
|
||||
prune-python-cache: true
|
||||
@@ -173,7 +176,7 @@ jobs:
|
||||
# HERMES_E2E_REQUIRE_TUI=1 below turns a missing build into a failure.
|
||||
uses: ./.github/actions/retry
|
||||
with:
|
||||
command: npm ci --workspace ui-tui --ignore-scripts --no-audit --no-fund && npm run build --workspace ui-tui
|
||||
command: pnpm install --frozen-lockfile --filter "{./ui-tui}..." --ignore-scripts && pnpm --dir ui-tui run build
|
||||
|
||||
- name: Install ripgrep (prebuilt binary)
|
||||
run: |
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
windows-bundle-tools:
|
||||
name: Cold Windows SDK (${{ matrix.runner }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# Cold ARM64 npm extraction alone can exceed 15 minutes on the hosted runner.
|
||||
# Cold ARM64 package extraction alone can exceed 15 minutes on the hosted runner.
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
cache-python: false
|
||||
cache-node: false
|
||||
- name: Install the locked bundle tooling without native postinstalls
|
||||
run: npm ci --workspace apps/desktop --workspace tests-js --include-workspace-root --include=dev --ignore-scripts --no-audit --no-fund
|
||||
run: pnpm install --frozen-lockfile --filter "{./apps/desktop}..." --filter "{./tests-js}..." --ignore-scripts
|
||||
- name: Provision from an empty cache and execute the SDK
|
||||
working-directory: apps/desktop
|
||||
run: node ../../node_modules/vitest/vitest.mjs run --project electron scripts/windows-bundle-tools.test.mjs scripts/msix-shared.test.mjs scripts/sign-msix.test.mjs scripts/batch-sign-binaries.test.mjs
|
||||
run: pnpm exec vitest run --project electron scripts/windows-bundle-tools.test.mjs scripts/msix-shared.test.mjs scripts/sign-msix.test.mjs scripts/batch-sign-binaries.test.mjs
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
extras: '[]'
|
||||
test-environment: 'true'
|
||||
|
||||
# Full npm ci (not --ignore-scripts): native addons need their lifecycle scripts.
|
||||
# Full install (not --ignore-scripts): native addons need their lifecycle scripts.
|
||||
- uses: ./.github/actions/retry
|
||||
with:
|
||||
command: npm ci
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
# Electron 42+ has no postinstall; unpack the binary the attach spec launches.
|
||||
- name: Prepare locked Electron runtime
|
||||
@@ -52,14 +52,14 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
shell: bash
|
||||
timeout-minutes: 15
|
||||
run: npm run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Attach spec on the dev tree
|
||||
id: dev
|
||||
working-directory: apps/desktop
|
||||
shell: bash
|
||||
timeout-minutes: 15
|
||||
run: npx playwright test -c e2e/core/playwright.config.ts local-gateway-attach.spec.ts
|
||||
run: pnpm exec playwright test -c e2e/core/playwright.config.ts local-gateway-attach.spec.ts
|
||||
env:
|
||||
CI: 'true'
|
||||
HERMES_E2E_PYTHON: ${{ steps.pm.outputs.python-path }}
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
shell: bash
|
||||
timeout-minutes: 15
|
||||
run: npm run builder -- --dir --publish never --win --x64
|
||||
run: pnpm run builder --dir --publish never --win --x64
|
||||
|
||||
- name: Attach spec on the packaged build
|
||||
if: always() && steps.dev.outcome != 'skipped'
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
if (-not $exe) { Get-ChildItem -Recurse release | Select-Object -First 50 FullName; throw 'no packaged exe under release/win*-unpacked' }
|
||||
"packaged exe: $($exe.FullName)"
|
||||
$env:HERMES_E2E_PACKAGED_EXE = $exe.FullName
|
||||
npx playwright test -c e2e/core/playwright.config.ts local-gateway-attach.spec.ts
|
||||
pnpm exec playwright test -c e2e/core/playwright.config.ts local-gateway-attach.spec.ts
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
env:
|
||||
CI: 'true'
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ apps/desktop/.staging-*/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Web UI assets — synced from @nous-research/ui at build time via
|
||||
# `npm run sync-assets` (see web/package.json).
|
||||
# `pnpm run sync-assets` (see web/package.json).
|
||||
web/public/fonts/
|
||||
web/public/ds-assets/
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# needed to prevent bad npm that has min-release-age but not exclude
|
||||
engine-strict=true
|
||||
|
||||
min-release-age=14
|
||||
# allow assistant-ui packages & a couple specific deps since they update a LOT.
|
||||
# remove this when we stabilize (or we haven't updated in 2 wks)
|
||||
min-release-age-exclude[]=@assistant-ui/*
|
||||
min-release-age-exclude[]=assistant-cloud
|
||||
min-release-age-exclude[]=assistant-stream
|
||||
min-release-age-exclude[]=@radix-ui/*
|
||||
min-release-age-exclude[]=radix-ui
|
||||
min-release-age-exclude[]=safe-content-frame
|
||||
|
||||
# react-router 8.3.0 includes fixes for vulns. remove this when 8.3.0 is > 2wks old.
|
||||
min-release-age-exclude[]=react-router
|
||||
|
||||
# eslint 10.8.0 includes fixes for vulns. remove this when 10.8.0 is > 2wks old.
|
||||
min-release-age-exclude[]=eslint
|
||||
min-release-age-exclude[]=@eslint/*
|
||||
|
||||
# tar 7.5.21 includes fixes for vulns. remove this when 7.5.21 is > 2wks old
|
||||
min-release-age-exclude[]=tar
|
||||
|
||||
# concurrently 10.0.4 includes fixes for vulns. remove this when 10.0.4 is > 2wks old
|
||||
min-release-age-exclude[]=concurrently
|
||||
|
||||
# fast-uri 3.1.4 includes fixes for vulns. remove this when 3.1.4 is > 2wks old
|
||||
min-release-age-exclude[]=fast-uri
|
||||
|
||||
# minimatch 10.2.6 includes fixes for vulns. remove this when 10.2.6 is > 2wks old
|
||||
min-release-age-exclude[]=minimatch
|
||||
|
||||
|
||||
# brace-expansion 5.0.9 includes fixes for vulns. remove this when 5.0.9 is > 2wks old
|
||||
min-release-age-exclude[]=brace-expansion
|
||||
|
||||
# js-yaml 4.3.1 includes fixes for GHSA-5p4m-2wfm-xmqj. remove when > 2wks old (rel 2026-07-31)
|
||||
min-release-age-exclude[]=js-yaml
|
||||
|
||||
# nanoid 3.3.17 includes fixes for GHSA-2v37-7h3g-55p8. remove when > 2wks old (rel 2026-08-03)
|
||||
min-release-age-exclude[]=nanoid
|
||||
|
||||
# dompurify 3.4.17 fixes GHSA-55q2-fjhq-7xh7 follow-ups (low). remove when 3.4.17 is > 2wks old (rel 2026-10-12)
|
||||
min-release-age-exclude[]=dompurify
|
||||
|
||||
# ink needs
|
||||
min-release-age-exclude[]=lightningcss
|
||||
min-release-age-exclude[]=postcss
|
||||
|
||||
# source-map-js 1.2.2 fixes GHSA-68fv-2mgg-jv7q (high, event-loop DoS). remove when 1.2.2 is > 2wks old (rel 2026-09-30)
|
||||
min-release-age-exclude[]=source-map-js
|
||||
|
||||
# http-cache-semantics 4.3.0 fixes GHSA-ch52-4w7c-c8xp (high, cross-user cached data disclosure).
|
||||
# remove when 4.3.0 is > 2wks old (rel 2026-10-04)
|
||||
min-release-age-exclude[]=http-cache-semantics
|
||||
|
||||
# ip-address 10.7.4 fixes GHSA-8xj3-p8x2-m3c7-family alerts (medium). remove when 10.7.4 is > 2wks old (rel 2026-10-11)
|
||||
min-release-age-exclude[]=ip-address
|
||||
|
||||
# sanitize-html 2.18.1 fixes GHSA-c9rx-x947-fp3h (medium), introduced by 2.18.0 which fixes
|
||||
# GHSA-x3q4-9hxx-gx8m/GHSA-cv27-6wvh-8x7j/GHSA-374f-7chj-9948. remove when 2.18.1 is > 2wks old (rel 2026-10-14)
|
||||
min-release-age-exclude[]=sanitize-html
|
||||
|
||||
# vitest 4.1.12 is a patch bump (4.1.11 -> 4.1.12). remove when 4.1.12 is > 2wks old (rel 2026-10-15)
|
||||
min-release-age-exclude[]=vitest
|
||||
min-release-age-exclude[]=@vitest/*
|
||||
|
||||
|
||||
# electron 44.9.3 backports upstream Chromium/V8 security fixes, and 44.9.1+ carries the
|
||||
# eMudhra root distrust per #142533. remove when 44.9.3 is > 2wks old (rel 2026-10-21)
|
||||
min-release-age-exclude[]=electron
|
||||
|
||||
# pele 1.13.0 is the release the desktop diagram swap was measured and parity-checked on (math
|
||||
# labels need 1.8.0+). remove when 1.13.0 is > 2wks old (rel 2026-10-31)
|
||||
min-release-age-exclude[]=pele
|
||||
+1
-1
@@ -111,7 +111,7 @@ hermes pm lock
|
||||
source ./activate
|
||||
```
|
||||
|
||||
Para JavaScript, ejecuta `npm ci` en el workspace correspondiente. Consulta
|
||||
Para JavaScript, ejecuta `pnpm install --frozen-lockfile` en la raíz del repositorio. Consulta
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md) para los requisitos completos.
|
||||
|
||||
---
|
||||
|
||||
+8
-3
@@ -119,6 +119,7 @@ The rules that most often send a submission back: the plugin must extend Hermes
|
||||
| **Git** | With the `git-lfs` extension installed |
|
||||
| **Python 3.14** | The project requires `>=3.14,<3.15`; PM provides the pinned interpreter |
|
||||
| **Node.js** | Use the PM pin, or a version accepted by root `package.json`: `>=26` |
|
||||
| **pnpm** | Exactly the root `package.json` `devEngines` pin; `source ./activate` provides it |
|
||||
|
||||
### PM developer environment
|
||||
|
||||
@@ -201,15 +202,19 @@ than copying production credentials into the checkout.
|
||||
|
||||
### JavaScript workspaces and website
|
||||
|
||||
From the repository root, run `npm ci` for the desktop, TUI, dashboard, and
|
||||
shared JS workspaces. The website is separate:
|
||||
From the repository root, run `pnpm install --frozen-lockfile` for the desktop,
|
||||
TUI, dashboard, and shared JS workspaces. Root `package.json` pins pnpm in
|
||||
`devEngines` (`source ./activate` provides that exact version). Hermes's builds and
|
||||
`hermes update` refuse any other pnpm version; a direct `pnpm install` does not check
|
||||
it. The root `preinstall` script refuses `npm install`. The website is a separate npm
|
||||
project:
|
||||
|
||||
```bash
|
||||
npm ci --prefix website
|
||||
npm run build:fast --prefix website
|
||||
```
|
||||
|
||||
Use a Node/npm version accepted by the corresponding `package.json` engines.
|
||||
Use a Node version accepted by the corresponding `package.json` engines.
|
||||
Native desktop dependencies can also require the platform build toolchain.
|
||||
|
||||
Logos and icons are generated from `assets/nous-girl-*.svg` and
|
||||
|
||||
+29
-22
@@ -314,14 +314,21 @@ RUN /opt/hermes/.venv/bin/python -I scripts/generate_icons.py --source /opt/herm
|
||||
|
||||
# Frontend dependencies never enter the runtime layers.
|
||||
FROM runtime_base AS frontend_build
|
||||
COPY package.json package-lock.json ./
|
||||
# pnpm installs the workspaces; nothing in the runtime image runs it, so only
|
||||
# the build stages (this one and webapp_build) stage it.
|
||||
RUN python3 -c 'from pathlib import Path; from pm import ensure, installed_package; ensure("pnpm", explicit=True); Path("/usr/local/bin/pnpm").symlink_to(installed_package("pnpm").binary)' && \
|
||||
pnpm --version
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY web/package.json web/
|
||||
COPY ui-tui/package.json ui-tui/
|
||||
COPY ui-tui/packages/hermes-ink/ ui-tui/packages/hermes-ink/
|
||||
COPY apps/shared/ apps/shared/
|
||||
COPY scripts/build/node-deps.mjs scripts/build/electron-runtime.mjs scripts/build/
|
||||
ENV npm_config_install_links=false
|
||||
RUN node scripts/build/node-deps.mjs --source /opt/hermes --workspace ui-tui --workspace web
|
||||
COPY scripts/build/node-deps.mjs scripts/build/electron-runtime.mjs scripts/build/pnpm-lock.mjs scripts/build/
|
||||
# Root node_modules/typescript is a link into pnpm's virtual store, which never
|
||||
# reaches the runtime image: stage a dereferenced copy for it.
|
||||
RUN node scripts/build/node-deps.mjs --source /opt/hermes --workspace ui-tui --workspace web && \
|
||||
mkdir -p /opt/products && \
|
||||
cp -RL node_modules/typescript /opt/products/typescript
|
||||
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY web/ web/
|
||||
@@ -337,12 +344,13 @@ RUN node scripts/build/tui.mjs --source /opt/hermes --out /opt/products/tui && \
|
||||
# which installs Desktop's dependencies in a private workspace. A stage of its
|
||||
# own so Desktop changes leave the TUI and dashboard layers cached.
|
||||
FROM runtime_base AS webapp_build
|
||||
COPY package.json package-lock.json .npmrc ./
|
||||
RUN python3 -c 'from pathlib import Path; from pm import ensure, installed_package; ensure("pnpm", explicit=True); Path("/usr/local/bin/pnpm").symlink_to(installed_package("pnpm").binary)' && \
|
||||
pnpm --version
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY apps/desktop/package.json apps/desktop/
|
||||
COPY apps/desktop/scripts/build-webapp.mjs apps/desktop/scripts/
|
||||
COPY apps/shared/package.json apps/shared/
|
||||
COPY scripts/build/*.mjs scripts/build/
|
||||
ENV npm_config_install_links=false
|
||||
# Dependencies first, keyed only by the manifests and the recipe, so a source
|
||||
# change reuses this layer and the build below reuses its install.
|
||||
RUN node apps/desktop/scripts/build-webapp.mjs --deps-only
|
||||
@@ -351,7 +359,7 @@ RUN node apps/desktop/scripts/build-webapp.mjs
|
||||
|
||||
FROM python_deps AS runtime
|
||||
# Standalone TypeScript linting is a runtime feature; Vite/esbuild are not.
|
||||
COPY --from=frontend_build /opt/hermes/node_modules/typescript /opt/hermes/node_modules/typescript
|
||||
COPY --from=frontend_build /opt/products/typescript /opt/hermes/node_modules/typescript
|
||||
RUN mkdir -p /opt/hermes/node_modules/.bin && \
|
||||
ln -s ../typescript/bin/tsc /opt/hermes/node_modules/.bin/tsc
|
||||
|
||||
@@ -473,22 +481,21 @@ COPY --chmod=0755 docker/cont-init.d/02-reconcile-profiles /etc/cont-init.d/02-r
|
||||
|
||||
# ---------- Runtime ----------
|
||||
ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist
|
||||
# Point the TUI launcher at the prebuilt bundle baked at build time (Layer 8:
|
||||
# `ui-tui && npm run build`). This makes _make_tui_argv take the prebuilt-bundle
|
||||
# fast path (`node --expose-gc /opt/hermes/ui-tui/dist/entry.js`) and skip the
|
||||
# _tui_need_npm_install / runtime `npm install` branch entirely — exactly the
|
||||
# nix/packaged-release path the launcher was designed for.
|
||||
# Point the TUI launcher at the prebuilt bundle baked at build time (the
|
||||
# frontend_build stage's scripts/build/tui.mjs product). This makes
|
||||
# _make_tui_argv take the prebuilt-bundle fast path
|
||||
# (`node --expose-gc /opt/hermes/ui-tui/dist/entry.js`) and skip source
|
||||
# dependency preparation (scripts/build/node-deps.mjs, a pnpm install)
|
||||
# entirely — exactly the nix/packaged-release path the launcher was designed for.
|
||||
#
|
||||
# Why this is required (not just an optimization): the root package-lock.json
|
||||
# describes the WHOLE monorepo workspace set (root + web + ui-tui + apps/*),
|
||||
# but the image only installs root/web/ui-tui (apps/* — the desktop app — is
|
||||
# never `npm install`ed here). So the actualized node_modules permanently
|
||||
# disagrees with the canonical lock, _tui_need_npm_install() returns True on
|
||||
# every launch, and the runtime `npm install` it triggers (a) can never
|
||||
# converge against the partial monorepo and (b) races itself across concurrent
|
||||
# embedded-chat (/api/pty) connections → ENOTEMPTY → the chat tab dies with a
|
||||
# 502 / "[session ended]". Pointing at the prebuilt bundle sidesteps the whole
|
||||
# check. (A separate launcher hardening is tracked independently.)
|
||||
# Why this is required (not just an optimization): the runtime image carries
|
||||
# no workspace node_modules (frontend dependencies stay in frontend_build; only
|
||||
# a TypeScript copy ships) and no pnpm. So the launcher's install check fails on
|
||||
# every launch, and the install it triggers (a) can never converge here and
|
||||
# (b) races itself across concurrent embedded-chat (/api/pty) connections →
|
||||
# ENOTEMPTY → the chat tab dies with a 502 / "[session ended]". Pointing at the
|
||||
# prebuilt bundle sidesteps the whole check. (A separate launcher hardening is
|
||||
# tracked independently.)
|
||||
ENV HERMES_TUI_DIR=/opt/hermes/ui-tui
|
||||
ENV HERMES_HOME=/opt/data
|
||||
ENV HERMES_WRITE_SAFE_ROOT=/opt/data
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ Ejecuta esto en PowerShell:
|
||||
iex (irm https://hermes-agent.nousresearch.com/install.ps1)
|
||||
```
|
||||
|
||||
El instalador de código fuente usa PM para Python 3.14, Node.js, npm,
|
||||
El instalador de código fuente usa PM para Python 3.14, Node.js, npm, pnpm,
|
||||
ripgrep, FFmpeg y las dependencias de Python. Si falta Git, descarga el archivo
|
||||
verificado de Git for Windows en el almacén de Hermes, sin reemplazar el Git
|
||||
del sistema. MSIX/App Installer es una distribución separada.
|
||||
|
||||
@@ -50,7 +50,7 @@ Run this in PowerShell:
|
||||
iex (irm https://hermes-agent.nousresearch.com/install.ps1)
|
||||
```
|
||||
|
||||
The source installer delegates Python 3.14, Node.js, npm, ripgrep, FFmpeg,
|
||||
The source installer delegates Python 3.14, Node.js, npm, pnpm, ripgrep, FFmpeg,
|
||||
and Python dependencies to PM. If Git is absent, it stages the verified Git
|
||||
for Windows archive in Hermes' tool store. It does not replace your system Git.
|
||||
See [installation methods](https://hermes-agent.nousresearch.com/docs/getting-started/installation)
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ iex (irm https://hermes-agent.nousresearch.com/install.ps1)
|
||||
|
||||
</div>
|
||||
|
||||
سورس انسٹالر Python 3.14، Node.js، npm، ripgrep، FFmpeg اور Python کی
|
||||
سورس انسٹالر Python 3.14، Node.js، npm، pnpm، ripgrep، FFmpeg اور Python کی
|
||||
ڈیپینڈینسیز کے لیے PM استعمال کرتا ہے۔ اگر Git موجود نہ ہو تو Git for Windows
|
||||
کا تصدیق شدہ آرکائیو ہرمیس کے ٹول اسٹور میں نصب کرتا ہے۔ سسٹم کا Git تبدیل نہیں
|
||||
ہوتا۔ MSIX/App Installer ایک الگ پیکیج ہے۔
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"tauri:build": "tauri build",
|
||||
"tauri:build:debug": "tauri build --debug",
|
||||
"typecheck": "tsc -p . --noEmit",
|
||||
"check": "npm run typecheck && npm run lint",
|
||||
"check": "pnpm run typecheck && pnpm run lint",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix",
|
||||
"fix": "npm run lint:fix"
|
||||
"fix": "pnpm run lint:fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nanostores/react": "1.1.0",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"version": "0.0.0",
|
||||
"identifier": "com.nousresearch.hermes.setup",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeDevCommand": "pnpm run dev",
|
||||
"devUrl": "http://127.0.0.1:5175",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"beforeBuildCommand": "pnpm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
|
||||
@@ -364,7 +364,7 @@ export async function openLogDir(): Promise<void> {
|
||||
// Dev-only isolated preview ("fake boot")
|
||||
//
|
||||
// Synthesises the manifest + stage/log events Rust normally streams, so the
|
||||
// whole reskin can be reviewed in a plain browser (`npm run dev`):
|
||||
// whole reskin can be reviewed in a plain browser (`pnpm run dev`):
|
||||
// ?fake=install welcome → [ INSTALL ] → success
|
||||
// ?fake=update auto-runs the granular update flow
|
||||
// ?fake=failure install that fails partway
|
||||
|
||||
@@ -118,8 +118,8 @@ python scripts/bundles/desktop.py --tag=vX.Y.Z
|
||||
Replace `vX.Y.Z` with an actual immutable tag. Stable tags must match the
|
||||
version in `pyproject.toml`. Canary tags use the release script's tag grammar.
|
||||
Start with host Python 3.11+ and Git. Use `python3` if that is your host's command.
|
||||
Preparation asks PM for the pinned Python, Node, npm and private installer;
|
||||
preinstalling a separate Node/npm/uv toolchain is not required. Native dependency
|
||||
Preparation asks PM for the pinned Python, Node, pnpm and private installer;
|
||||
preinstalling a separate Node/pnpm/uv toolchain is not required. Native dependency
|
||||
builds still need the platform's compiler, SDK and libraries. Windows ARM64 uses
|
||||
the shared Visual Studio/Clang/Rust/static OpenSSL preparation provider, which
|
||||
can require Administrator permissions for missing system components. macOS
|
||||
@@ -378,8 +378,8 @@ separate development home. Activation supplies the toolchain, not `node_modules`
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npm run dev --workspace apps/desktop
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm --dir apps/desktop run dev
|
||||
```
|
||||
|
||||
For an ordinary package of that desktop build, use the workspace's
|
||||
|
||||
@@ -272,10 +272,10 @@ Verify scheduling changes with native hidden/minimized windows and real preview
|
||||
navigation, typing and capture. Browser automation's focus emulation can keep
|
||||
hidden frames awake and conceal regressions; disable it for these checks.
|
||||
Software-rendered VM timings are not hardware GPU or battery measurements.
|
||||
With a graphical session (or Xvfb), `npm run test:preview-activity-native
|
||||
--workspace apps/desktop` exercises real hidden-frame navigation (including
|
||||
With a graphical session (or Xvfb), `pnpm --dir apps/desktop run
|
||||
test:preview-activity-native` exercises real hidden-frame navigation (including
|
||||
lease-only rAF and capture), overlapping operations, cancellation and plugin
|
||||
completion values in a disposable profile. Add `-- --minimized` with a running
|
||||
completion values in a disposable profile. Add `--minimized` with a running
|
||||
window manager to exercise actual iconification rather than hiding the window.
|
||||
|
||||
## Nous free tier: state is pulled, never latched in the renderer
|
||||
|
||||
+16
-16
@@ -84,28 +84,28 @@ shortcut helper. Prebuilt installers include it; no compiler is needed at runtim
|
||||
Want to hack on the app itself? Install workspace deps from the repo root once, then run the dev server from this directory:
|
||||
|
||||
```bash
|
||||
npm install # from repo root — links apps/desktop, web, apps/shared
|
||||
pnpm install # from repo root — links apps/desktop, web, apps/shared
|
||||
cd apps/desktop
|
||||
npm run dev # Vite renderer + Electron, which boots the Python backend
|
||||
pnpm run dev # Vite renderer + Electron, which boots the Python backend
|
||||
```
|
||||
|
||||
Point the app at a specific source checkout, or sandbox it away from your real config:
|
||||
|
||||
```bash
|
||||
# throwaway HERMES_HOME, separate Electron userData, distinct app name to avoid the single-instance lock
|
||||
../../scripts/dev-sandbox.sh npm run dev
|
||||
HERMES_DESKTOP_HERMES_ROOT=/path/to/clone npm run dev
|
||||
HERMES_HOME=$HOME/.hermes/cache/scratch/throwaway npm run dev
|
||||
npm run dev:fake-boot # exercise the startup overlay with deterministic delays
|
||||
../../scripts/dev-sandbox.sh pnpm run dev
|
||||
HERMES_DESKTOP_HERMES_ROOT=/path/to/clone pnpm run dev
|
||||
HERMES_HOME=$HOME/.hermes/cache/scratch/throwaway pnpm run dev
|
||||
pnpm run dev:fake-boot # exercise the startup overlay with deterministic delays
|
||||
```
|
||||
|
||||
### Building installers
|
||||
|
||||
```bash
|
||||
npm run dist:mac # DMG + zip
|
||||
npm run dist:win # MSIX
|
||||
npm run dist:linux # AppImage + deb + rpm
|
||||
npm run pack # unpacked app under release/ (no installer)
|
||||
pnpm run dist:mac # DMG + zip
|
||||
pnpm run dist:win # MSIX
|
||||
pnpm run dist:linux # AppImage + deb + rpm
|
||||
pnpm run pack # unpacked app under release/ (no installer)
|
||||
```
|
||||
|
||||
These are ordinary packaging commands, not complete tagged payload builds.
|
||||
@@ -217,14 +217,14 @@ and retained background sockets continue receiving events from running jobs.
|
||||
Run before opening a PR (lint may surface pre-existing warnings but must exit cleanly):
|
||||
|
||||
```bash
|
||||
npm run fix
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run test:ui
|
||||
npm run test:desktop:platforms
|
||||
pnpm run fix
|
||||
pnpm run typecheck
|
||||
pnpm run lint
|
||||
pnpm run test:ui
|
||||
pnpm run test:desktop:platforms
|
||||
```
|
||||
|
||||
Run `npm run test:desktop:all` for install, boot, update, packaging, or other
|
||||
Run `pnpm run test:desktop:all` for install, boot, update, packaging, or other
|
||||
release-path changes.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
* does) instead of quietly writing plaintext. See the branch comments in each
|
||||
* test for the reasoning.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* resolution fails with a controlled error message. The app should show the
|
||||
* BootFailureOverlay with retry/repair actions.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import { allowErrorBanners, test } from './test'
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
*
|
||||
* electron → hermes serve (python) → mock provider → renderer
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
* Run from the nix devshell:
|
||||
* npm exec playwright test e2e/boot.spec.ts --reporter=list
|
||||
* pnpm exec playwright test e2e/boot.spec.ts --reporter=list
|
||||
*/
|
||||
import { expect, test } from './test'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* provider). The mock server returns a canned reply, so we verify the
|
||||
* response text shows up in the chat transcript.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import { runDesktopChatSmoke } from '../../../tests-js/scripts/desktop-chat-smoke'
|
||||
|
||||
@@ -94,11 +94,11 @@ by the turn's own marker), no visual baselines, `retries: 0`, one worker,
|
||||
sandboxed `HOME`/`HERMES_HOME`/user-data per test, all `HERMES_*` and
|
||||
credential env stripped from the spawned app.
|
||||
|
||||
Run locally (Linux, after `npm run build` in `apps/desktop`):
|
||||
Run locally (Linux, after `pnpm run build` in `apps/desktop`):
|
||||
|
||||
```sh
|
||||
cd apps/desktop
|
||||
xvfb-run -a npx playwright test -c e2e/core/playwright.config.ts
|
||||
xvfb-run -a pnpm exec playwright test -c e2e/core/playwright.config.ts
|
||||
```
|
||||
|
||||
`HERMES_E2E_CORE_ROOT` picks the sandbox parent dir (default: OS tmpdir);
|
||||
|
||||
@@ -204,7 +204,7 @@ export function webappLaunchLink(sandbox: CoreSandbox, profile?: string): string
|
||||
|
||||
export async function launchCoreApp(env: Record<string, string>): Promise<{ app: ElectronApplication; page: Page }> {
|
||||
if (!fs.existsSync(path.join(DESKTOP_ROOT, 'dist', 'electron-main.mjs'))) {
|
||||
throw new Error("Desktop dist not built: run 'npm run build' in apps/desktop first")
|
||||
throw new Error("Desktop dist not built: run 'pnpm run build' in apps/desktop first")
|
||||
}
|
||||
|
||||
const app = await _electron.launch({
|
||||
|
||||
@@ -129,7 +129,7 @@ function packagedBuildOrSkip(): PackagedBuild {
|
||||
if (missing.length > 0) {
|
||||
const reason =
|
||||
`no packaged build (missing ${missing.join(', ')}): run ` +
|
||||
"'npm run build && npm run builder -- --dir --publish never --linux' in apps/desktop"
|
||||
"'pnpm run build && pnpm run builder --dir --publish never --linux' in apps/desktop"
|
||||
|
||||
if (process.env.CI && process.env.HERMES_E2E_REQUIRE_PACKAGED === '1') {
|
||||
throw new Error(`HERMES_E2E_REQUIRE_PACKAGED=1 but ${reason}`)
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
* Webapp turn raised, and the Webapp stops a turn Electron started. A third puts the Webapp behind
|
||||
* the sign-in gate: `hermes webapp --sign-out` signs out a browser that signed in with a password.
|
||||
*
|
||||
* Linux (the /proc ancestry walk); needs the Webapp renderer (`npm run build:webapp`) and a
|
||||
* Chromium Playwright can launch (`npx playwright install chromium`, or HERMES_E2E_CHROMIUM).
|
||||
* Linux (the /proc ancestry walk); needs the Webapp renderer (`pnpm run build:webapp`) and a
|
||||
* Chromium Playwright can launch (`pnpm exec playwright install chromium`, or HERMES_E2E_CHROMIUM).
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
@@ -100,7 +100,7 @@ test('the gateway serves the Webapp and runs every turn its browser sends', asyn
|
||||
test.skip(process.platform !== 'linux', 'walks /proc to prove which process ran the tool')
|
||||
expect(
|
||||
fs.existsSync(path.join(DESKTOP_ROOT, 'dist-webapp', 'index.html')),
|
||||
'the Webapp renderer is built (npm run build:webapp)'
|
||||
'the Webapp renderer is built (pnpm run build:webapp)'
|
||||
).toBe(true)
|
||||
|
||||
const provider = await startScriptedProvider()
|
||||
@@ -218,7 +218,7 @@ test('a Webapp tab opened on a named profile runs and reads that profile', async
|
||||
test.setTimeout(420_000)
|
||||
expect(
|
||||
fs.existsSync(path.join(DESKTOP_ROOT, 'dist-webapp', 'index.html')),
|
||||
'the Webapp renderer is built (npm run build:webapp)'
|
||||
'the Webapp renderer is built (pnpm run build:webapp)'
|
||||
).toBe(true)
|
||||
|
||||
const provider = await startScriptedProvider()
|
||||
@@ -298,7 +298,7 @@ test("the Webapp and Electron Desktop answer and stop each other's turns on one
|
||||
test.setTimeout(480_000)
|
||||
expect(
|
||||
fs.existsSync(path.join(DESKTOP_ROOT, 'dist-webapp', 'index.html')),
|
||||
'the Webapp renderer is built (npm run build:webapp)'
|
||||
'the Webapp renderer is built (pnpm run build:webapp)'
|
||||
).toBe(true)
|
||||
|
||||
const provider = await startScriptedProvider()
|
||||
@@ -448,7 +448,7 @@ test('signing out of a Webapp behind sign-in signs the browser out', async () =>
|
||||
test.skip(process.platform !== 'linux', 'runs with the other Webapp journeys (Linux CI)')
|
||||
expect(
|
||||
fs.existsSync(path.join(DESKTOP_ROOT, 'dist-webapp', 'index.html')),
|
||||
'the Webapp renderer is built (npm run build:webapp)'
|
||||
'the Webapp renderer is built (pnpm run build:webapp)'
|
||||
).toBe(true)
|
||||
|
||||
const provider = await startScriptedProvider()
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* the only copy of the streamed reply, and the reopened chat shows it.
|
||||
*
|
||||
* Real gateway, real Webapp renderer, real browser storage; only the model is scripted. Linux, like
|
||||
* the other Webapp journeys; needs `npm run build:webapp` and a Chromium Playwright can launch.
|
||||
* the other Webapp journeys; needs `pnpm run build:webapp` and a Chromium Playwright can launch.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs'
|
||||
@@ -226,7 +226,7 @@ async function withWebapp(
|
||||
test.skip(process.platform !== 'linux', 'runs with the other Webapp journeys (Linux CI)')
|
||||
expect(
|
||||
fs.existsSync(path.join(DESKTOP_ROOT, 'dist-webapp', 'index.html')),
|
||||
'the Webapp renderer is built (npm run build:webapp)'
|
||||
'the Webapp renderer is built (pnpm run build:webapp)'
|
||||
).toBe(true)
|
||||
|
||||
const provider = await startScriptedProvider()
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*
|
||||
* CROSS_GATEWAY_LEG=main records the picker (the repro: Homelab's bot absent)
|
||||
* and stops. CROSS_GATEWAY_SHOTS=<dir> saves screenshots; PROBE_RESULT lines
|
||||
* carry the evidence. Needs `npm run build` (dist/) first.
|
||||
* carry the evidence. Needs `pnpm run build` (dist/) first.
|
||||
*/
|
||||
|
||||
import { type ChildProcess, spawn, spawnSync } from 'node:child_process'
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
*
|
||||
* Three rules the previous single-path probe got wrong:
|
||||
*
|
||||
* 1. The binary is not always under the REPO ROOT. This is an npm workspaces
|
||||
* repo, and npm only hoists a dependency to the root when nothing conflicts
|
||||
* — otherwise `electron` installs into `apps/desktop/node_modules`. Both
|
||||
* layouts are normal, so both have to be searched, nearest package first.
|
||||
* 1. The binary is not under the REPO ROOT. pnpm links `electron` into
|
||||
* `apps/desktop/node_modules`, the workspace that declares it, and never
|
||||
* hoists it to the root.
|
||||
* 2. The binary is `electron.exe` on Windows. A bare `electron` never exists
|
||||
* there, so the probe could only ever miss.
|
||||
* 3. `which` is not a command on Windows. The PATH fallback spawned it
|
||||
* unconditionally, so on Windows the fallback failed for the wrong reason
|
||||
* and the error message blamed a missing `npm install`.
|
||||
* and the error message blamed a missing install.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
@@ -29,8 +28,7 @@ export function electronBinaryName(platform: NodeJS.Platform = process.platform)
|
||||
}
|
||||
|
||||
/**
|
||||
* Where an npm install can leave the binary, in probe order: nearest package
|
||||
* first, so a workspace-local install wins over a stale hoisted one.
|
||||
* Where an install leaves the binary under each root, in probe order.
|
||||
*/
|
||||
export function electronDistCandidates(roots: string[], platform: NodeJS.Platform = process.platform): string[] {
|
||||
return roots.map((root) => path.join(root, 'node_modules', 'electron', 'dist', electronBinaryName(platform)))
|
||||
@@ -64,7 +62,7 @@ export function electronPackagePath(from: string): null | string {
|
||||
/**
|
||||
* Resolve the Electron binary, or throw with the layouts that were searched.
|
||||
*
|
||||
* `roots` are searched in order; pass the desktop package before the repo root.
|
||||
* `roots` are searched in order.
|
||||
*/
|
||||
export function resolveElectronBinary(roots: string[]): string {
|
||||
for (const root of roots) {
|
||||
@@ -95,6 +93,6 @@ export function resolveElectronBinary(roots: string[]): string {
|
||||
|
||||
throw new Error(
|
||||
`Electron binary not found. Searched ${electronDistCandidates(roots).join(', ')} and PATH. ` +
|
||||
'Run "npm install" from the repo root to install devDependencies.',
|
||||
'Run "pnpm install --frozen-lockfile" from the repo root to install devDependencies.',
|
||||
)
|
||||
}
|
||||
@@ -12,11 +12,10 @@ describe('electronDistCandidates', () => {
|
||||
const desktop = path.join('repo', 'apps', 'desktop')
|
||||
const repo = 'repo'
|
||||
|
||||
it('probes the workspace-local install before the hoisted one', () => {
|
||||
// npm only hoists `electron` to the repo root when nothing conflicts, so
|
||||
// apps/desktop/node_modules is an ordinary outcome of `npm install`, not a
|
||||
// broken tree. Probing only the repo root is what makes the suite refuse to
|
||||
// start with "run npm install" on a tree that has electron installed.
|
||||
it('probes each root in the order given', () => {
|
||||
// The nearest package comes first: probing only the repo root is what made
|
||||
// the suite refuse to start on a tree whose electron lives in the desktop
|
||||
// workspace, where pnpm links it.
|
||||
expect(electronDistCandidates([desktop, repo], 'linux')).toEqual([
|
||||
path.join(desktop, 'node_modules', 'electron', 'dist', 'electron'),
|
||||
path.join(repo, 'node_modules', 'electron', 'dist', 'electron'),
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* `electron-builder --dir` step and matches `hermes desktop --source`. The
|
||||
* packaged-binary path is already covered by `launch.spec.ts`.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so that `dist/` exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so that `dist/` exists.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs'
|
||||
@@ -246,14 +246,14 @@ function assertDistBuilt(): void {
|
||||
|
||||
if (!fs.existsSync(electronMain)) {
|
||||
throw new Error(
|
||||
`Desktop dist not built. Run 'cd apps/desktop && npm run build' first.\n` +
|
||||
`Desktop dist not built. Run 'cd apps/desktop && pnpm run build' first.\n` +
|
||||
`Missing: ${electronMain}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(indexHtml)) {
|
||||
throw new Error(
|
||||
`Desktop dist/index.html not found. Run 'cd apps/desktop && npm run build' first.\n` +
|
||||
`Desktop dist/index.html not found. Run 'cd apps/desktop && pnpm run build' first.\n` +
|
||||
`Missing: ${indexHtml}`,
|
||||
)
|
||||
}
|
||||
@@ -261,18 +261,17 @@ function assertDistBuilt(): void {
|
||||
|
||||
/**
|
||||
* Find the Electron binary. In the nix devshell, `electron` is on PATH.
|
||||
* As a fallback, use the node_modules/electron install from either package.
|
||||
* As a fallback, use the desktop package's node_modules/electron install.
|
||||
*/
|
||||
export function findElectron(): string {
|
||||
// In dev mode, we use the `electron` binary directly (not the packaged app).
|
||||
// The dev:electron script in package.json does exactly this: `electron .`
|
||||
// after building. We replicate that here.
|
||||
//
|
||||
// The desktop package is searched first: npm workspaces only hoist
|
||||
// `electron` to the repo root when nothing conflicts, so a workspace-local
|
||||
// install is just as ordinary an outcome as a hoisted one. The rules live in
|
||||
// ./electron-binary so they can be unit-tested per platform.
|
||||
return resolveElectronBinary([DESKTOP_ROOT, REPO_ROOT])
|
||||
// pnpm links `electron` into the desktop package, the workspace that
|
||||
// declares it. The rules live in ./electron-binary so they can be
|
||||
// unit-tested per platform.
|
||||
return resolveElectronBinary([DESKTOP_ROOT])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,19 +488,19 @@ export interface PackagedAppFixture {
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the *packaged* Electron binary (from `npm run pack` →
|
||||
* Launch the *packaged* Electron binary (from `pnpm run pack` →
|
||||
* `electron-builder --dir`) with `BOOT_FAKE=1` so it simulates boot
|
||||
* progress without spawning a real Hermes backend.
|
||||
*
|
||||
* Uses the same sandbox isolation (credential stripping, isolated
|
||||
* HERMES_HOME + userData, unique app name) as the dev-mode fixtures.
|
||||
*
|
||||
* Skips if the packaged binary doesn't exist — run `npm run pack` first.
|
||||
* Skips if the packaged binary doesn't exist — run `pnpm run pack` first.
|
||||
*/
|
||||
export async function setupPackagedApp(): Promise<PackagedAppFixture> {
|
||||
if (!packagedBinaryExists()) {
|
||||
throw new Error(
|
||||
`Built app binary not found: ${PACKAGED_BINARY_PATH}. Run 'npm run pack' first.`,
|
||||
`Built app binary not found: ${PACKAGED_BINARY_PATH}. Run 'pnpm run pack' first.`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* therefore performs the same dial → commit → re-home the statusbar switcher
|
||||
* does, against a real backend — not a stub.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists, and the
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists, and the
|
||||
* repo's Python venv (`.venv`) must exist for both backends.
|
||||
*/
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
* The test pins that converged single-message shape: every text
|
||||
* appears exactly once, inside a single assistant message root.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import { expect, type Page, test } from '@playwright/test'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* should detect the unconfigured state and show the DesktopOnboardingOverlay
|
||||
* with provider options / API key form.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import { test } from './test'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* poll stops once A's pane is hidden. Before the fix the row kept its
|
||||
* "Background task running" state until the user clicked back into A.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* shared word must surface that profile's marker and never the other one's,
|
||||
* across default → ssprobe-b → default.
|
||||
*
|
||||
* Prerequisite: `npm run build` (dist/) and the repo's `.venv`.
|
||||
* Prerequisite: `pnpm run build` (dist/) and the repo's `.venv`.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* real (trivial commands + real subagent delegations). The tests assert the
|
||||
* sidebar states driven by real gateway events.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* 5. Open session A → dot clears.
|
||||
* 6. Restart once more → the cleared state must persist too (no zombie dot).
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import { expect, type Page, test } from '@playwright/test'
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
* must not rebuild the kept-alive transcript: zero additive bursts and zero
|
||||
* reconciles.
|
||||
*
|
||||
* Prerequisite: `npm run build` must have been run so dist/ exists.
|
||||
* Prerequisite: `pnpm run build` must have been run so dist/ exists.
|
||||
*/
|
||||
|
||||
import { writeEnvFile, writeMockProviderConfig } from '../../../tests-js/scripts/mock-provider-config'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for electron/backend-probes.ts.
|
||||
*
|
||||
* Run with: node --test electron/backend-probes.test.ts
|
||||
* (Wired into npm test:desktop:platforms in package.json.)
|
||||
* (Wired into pnpm run test:desktop:platforms in package.json.)
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for electron/backend-ready.ts.
|
||||
*
|
||||
* Run with: node --test electron/backend-ready.test.ts
|
||||
* (Wired into npm test:desktop:platforms in package.json.)
|
||||
* (Wired into pnpm run test:desktop:platforms in package.json.)
|
||||
*
|
||||
* Covers the cold-start port-announcement deadline (issue #50209): the clock
|
||||
* starts before the backend binds its port, so a tight 45s deadline killed a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for electron/composer-image-name.ts.
|
||||
*
|
||||
* Run with: vitest run --project electron composer-image-name
|
||||
* (Wired into npm test:desktop:platforms in package.json.)
|
||||
* (Wired into pnpm run test:desktop:platforms in package.json.)
|
||||
*
|
||||
* Regression (issue #96403): pasted composer screenshots were named with a
|
||||
* UTC timestamp (Date.toISOString()), so the filename disagreed with the
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for electron/connection-config.ts.
|
||||
*
|
||||
* Run with: node --test electron/connection-config.test.ts
|
||||
* (Wire into npm test:desktop:platforms in package.json.)
|
||||
* (Wire into pnpm run test:desktop:platforms in package.json.)
|
||||
*
|
||||
* These are the pure helpers behind the remote-gateway connection settings:
|
||||
* URL normalization, WS-URL construction (token vs OAuth ticket), auth-mode
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for electron/dashboard-token.ts.
|
||||
*
|
||||
* Run with: node --test electron/dashboard-token.test.ts
|
||||
* (Wired into npm test:desktop:platforms in package.json.)
|
||||
* (Wired into pnpm run test:desktop:platforms in package.json.)
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*
|
||||
* ``apps/desktop/package.json`` declared ``electronVersion: 40.9.3`` (the tested,
|
||||
* JS-extract build) but pinned the dependency loosely as ``electron: ^40.9.3``.
|
||||
* ``npm ci`` then resolved 40.10.3/40.10.4 — the new *native* extract-zip whose
|
||||
* npm then resolved 40.10.3/40.10.4 — the new *native* extract-zip whose
|
||||
* win32 binding imports VCRUNTIME140.dll and fails to ``dlopen`` on hosts without
|
||||
* the VC++ runtime (``ERR_DLOPEN_FAILED loading index.win32-x64-msvc.node``).
|
||||
* scripts/build/electron-runtime.mjs now supplies that DLL.
|
||||
@@ -20,8 +20,8 @@
|
||||
*
|
||||
* 1. the Electron dependency is an *exact* version (Electron Builder needs the
|
||||
* installed binary to match ``electronVersion`` / ``electronDist``), and
|
||||
* 2. the dependency and the independently resolved lockfile entry agree — so ``npm ci`` installs exactly what
|
||||
* the build packages.
|
||||
* 2. the dependency and the independently resolved lockfile entry agree — so a frozen install installs exactly
|
||||
* what the build packages.
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -29,10 +29,11 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { test } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..')
|
||||
const DESKTOP_PKG = path.join(REPO_ROOT, 'apps', 'desktop', 'package.json')
|
||||
const ROOT_LOCK = path.join(REPO_ROOT, 'package-lock.json')
|
||||
const ROOT_LOCK = path.join(REPO_ROOT, 'pnpm-lock.yaml')
|
||||
|
||||
// An exact semver: digits.digits.digits with an optional prerelease/build tag,
|
||||
// but NO range operators (^ ~ > < = * x || spaces || -range).
|
||||
@@ -63,33 +64,43 @@ test('electron dependency is exactly pinned', () => {
|
||||
spec,
|
||||
EXACT_SEMVER,
|
||||
`electron must be pinned to an exact version, got "${spec}". ` +
|
||||
'A range (^/~) lets npm ci resolve a newer Electron whose postinstall ' +
|
||||
'A range (^/~) lets a lockfile update resolve a newer Electron whose postinstall ' +
|
||||
'may differ from the one the build was validated against.'
|
||||
)
|
||||
})
|
||||
|
||||
interface LockDependency {
|
||||
specifier: string
|
||||
version: string
|
||||
}
|
||||
|
||||
interface Lockfile {
|
||||
importers?: Record<string, Partial<Record<'dependencies' | 'devDependencies', Record<string, LockDependency>>>>
|
||||
packages?: Record<string, unknown>
|
||||
}
|
||||
|
||||
test('lockfile resolves the pinned electron', () => {
|
||||
if (!fs.existsSync(ROOT_LOCK)) {
|
||||
return
|
||||
} // skip if lockfile not present
|
||||
|
||||
const spec = electronSpec(desktopPkg())
|
||||
const lock = JSON.parse(fs.readFileSync(ROOT_LOCK, 'utf-8'))
|
||||
const packages = (lock.packages ?? {}) as Record<string, { version?: string }>
|
||||
const lock = parse(fs.readFileSync(ROOT_LOCK, 'utf-8')) as Lockfile
|
||||
const importer = lock.importers?.['apps/desktop']
|
||||
const entry = importer?.devDependencies?.electron ?? importer?.dependencies?.electron
|
||||
|
||||
const resolved = Object.entries(packages)
|
||||
.filter(([key]) => key.endsWith('node_modules/electron'))
|
||||
.map(([, meta]) => meta.version)
|
||||
.filter((v): v is string => !!v)
|
||||
assert.ok(entry, 'no apps/desktop electron entry found in pnpm-lock.yaml')
|
||||
|
||||
assert.ok(resolved.length > 0, 'no electron entry found in package-lock.json')
|
||||
// A locked version carries its peer resolution as a suffix: 44.9.3(supports-color@10.2.2).
|
||||
const resolved = [
|
||||
entry.version.replace(/\(.*$/, ''),
|
||||
...Object.keys(lock.packages ?? {})
|
||||
.filter(key => key.startsWith('electron@'))
|
||||
.map(key => key.slice('electron@'.length))
|
||||
]
|
||||
|
||||
for (const v of resolved) {
|
||||
assert.equal(
|
||||
v,
|
||||
spec,
|
||||
`package-lock.json resolves electron to ${v}, but the pin is "${spec}"; ` +
|
||||
'run `npm install --package-lock-only` so `npm ci` stays consistent.'
|
||||
`pnpm-lock.yaml resolves electron to ${v}, but the pin is "${spec}"; ` +
|
||||
'run `pnpm install --lockfile-only` so frozen installs stay consistent.'
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for electron/desktop-uninstall.ts.
|
||||
*
|
||||
* Run with: node --test electron/desktop-uninstall.test.ts
|
||||
* (Wired into npm test:desktop:platforms in package.json.)
|
||||
* (Wired into pnpm run test:desktop:platforms in package.json.)
|
||||
*
|
||||
* These are the pure helpers behind the desktop Chat GUI uninstaller: the
|
||||
* mode → CLI-flag mapping, the running-app-bundle resolution per OS, and the
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Tests for electron/dev-cdp.ts.
|
||||
*
|
||||
* Run with: npx vitest run --project electron electron/dev-cdp.test.ts
|
||||
* Run with: pnpm exec vitest run --project electron electron/dev-cdp.test.ts
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -12,7 +12,7 @@ import { DEFAULT_PORT, describeDevCdpDecision, resolveDevCdpPort } from './dev-c
|
||||
|
||||
const DEV_SERVER = 'http://127.0.0.1:5174'
|
||||
|
||||
/** The ordinary `npm run dev` / `hgui` run. */
|
||||
/** The ordinary `pnpm run dev` / `hgui` run. */
|
||||
const devRun = { env: {}, isPackaged: false, devServer: DEV_SERVER }
|
||||
|
||||
test('a dev-server run opens the default port with no opt-in', () => {
|
||||
|
||||
@@ -59,7 +59,7 @@ function resolveDevCdpPort({ env, isPackaged, devServer }: DevCdpInput): DevCdpD
|
||||
return { port: null, reason: 'packaged' }
|
||||
}
|
||||
|
||||
// A dev server means a source-tree run (`npm run dev` / `hgui`).
|
||||
// A dev server means a source-tree run (`pnpm run dev` / `hgui`).
|
||||
if (!devServer) {
|
||||
return { port: null, reason: 'no-dev-server' }
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ const args = wslgLaunchArgs(
|
||||
)
|
||||
|
||||
if (args) {
|
||||
// Keep the launcher alive until the child exits: npm's concurrently must not
|
||||
// Keep the launcher alive until the child exits: the dev script's concurrently must not
|
||||
// tear down Vite during this handoff. No backend, windows or single-instance
|
||||
// lock are created in this parent. The child has an explicit platform flag,
|
||||
// so it goes straight into main on its first pass.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for electron/gateway-ws-probe.ts.
|
||||
*
|
||||
* Run with: node --test electron/gateway-ws-probe.test.ts
|
||||
* (Wired into npm test:desktop:platforms in package.json.)
|
||||
* (Wired into pnpm run test:desktop:platforms in package.json.)
|
||||
*
|
||||
* The probe drives a real WebSocket handshake for the "Test remote" button.
|
||||
* Here we inject a fake socket so we can deterministically replay each handshake
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Type declarations for the get-windows optionalDependency.
|
||||
//
|
||||
// get-windows ships no bundled types and is an optionalDependency. `npm ci`
|
||||
// get-windows ships no bundled types and is an optionalDependency. The install
|
||||
// can skip it when its native install fails, including Linux and Windows ARM64
|
||||
// where 9.3.0 has no prebuilt, so it can legitimately be absent from
|
||||
// node_modules. Declaring the module here keeps typecheck independent of
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// install-stamp.ts — the typed build-time install stamp.
|
||||
//
|
||||
// scripts/write-build-stamp.mjs writes build/install-stamp.json during
|
||||
// `npm run build`.
|
||||
// `pnpm run build`.
|
||||
// bundle-electron-main.mjs bakes that file into the
|
||||
// production bundle by defining the __HERMES_INSTALL_STAMP__ global as
|
||||
// the stamp. The stamp is a constant of the artifact.
|
||||
|
||||
@@ -794,7 +794,7 @@ let f12Blocked = false
|
||||
|
||||
// Preload must be plain JS — Electron's sandbox can't run .ts, and tsx's
|
||||
// ESM loader is broken on Electron 40's Node (ERR_INVALID_RETURN_PROPERTY_VALUE).
|
||||
// Dev (`npm run dev`) and prod both load the esbuild output from dist/.
|
||||
// Dev (`pnpm run dev`) and prod both load the esbuild output from dist/.
|
||||
const PRELOAD_PATH = path.join(APP_ROOT, 'dist', 'electron-preload.js')
|
||||
const PREVIEW_GUEST_PRELOAD_PATH = path.join(APP_ROOT, 'dist', 'preview-guest-preload.js')
|
||||
|
||||
@@ -867,7 +867,7 @@ if (IS_WINDOWS) {
|
||||
}
|
||||
}
|
||||
|
||||
// Renderer debugging port. On for dev-server runs (`hgui` / `npm run dev`) so
|
||||
// Renderer debugging port. On for dev-server runs (`hgui` / `pnpm run dev`) so
|
||||
// the CDP tooling in scripts/ can attach; never for a packaged build — see
|
||||
// electron/dev-cdp.ts. Must run before app `ready` like the switches above;
|
||||
// Chromium binds it at launch.
|
||||
@@ -4853,7 +4853,7 @@ async function resolveHermesBackend(backendArgs: string[]): Promise<ResolvedHerm
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Development source -- when running `npm run dev` from a checkout, the
|
||||
// 2. Development source -- when running `pnpm run dev` from a checkout, the
|
||||
// cloned repo at SOURCE_REPO_ROOT takes precedence over ACTIVE and any
|
||||
// installed `hermes` on PATH so local Python edits are actually exercised.
|
||||
// (In dev with no checkout, SOURCE_REPO_ROOT won't pass isHermesSourceRoot.)
|
||||
@@ -17135,7 +17135,7 @@ ipcMain.handle('hermes:version', async (_event, scope?: { connectionId?: string;
|
||||
// True when the bundle on disk is not the one this process loaded — a
|
||||
// plain app restart (no rebuild, no installer) clears the skew above.
|
||||
// Packaged only: a dev `--build-only` rewrites build/install-stamp.json
|
||||
// under a running `npm start`, which is a rebuild the developer asked for,
|
||||
// under a running `pnpm start`, which is a rebuild the developer asked for,
|
||||
// not a torn install to offer a restart for.
|
||||
bundleSwapPending: IS_PACKAGED && detectBundleSwap(INSTALL_STAMP, readBundleSwapStamp(process.resourcesPath))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// so a non-executable helper fails every terminal spawn with
|
||||
// `Error: posix_spawnp failed.`. Packaged builds are covered because
|
||||
// stage-native-deps.mjs chmods the staged copy, but the dev flow
|
||||
// (`npm run dev` → `electron .`) resolves node-pty straight from
|
||||
// (`pnpm run dev` → `electron .`) resolves node-pty straight from
|
||||
// `node_modules/`, which nobody chmods. This restores the exec bits at runtime,
|
||||
// best-effort, so both dev and any environment that stripped the bit keep
|
||||
// working. Idempotent: files that are already executable are left untouched.
|
||||
|
||||
@@ -171,10 +171,12 @@ export function registerTerminalIpc({
|
||||
function terminalShellEnv() {
|
||||
const env = { ...process.env }
|
||||
|
||||
// Electron is commonly launched through `npm run dev`; do not leak npm's
|
||||
// managed prefix into a user's interactive shell (nvm/proto warn loudly).
|
||||
// Electron is commonly launched through `pnpm run dev`; do not leak the
|
||||
// package manager's settings into a user's interactive shell (nvm/proto warn
|
||||
// loudly, and the user's own pnpm would inherit them). Package managers inject
|
||||
// lowercase names: a user's own NPM_CONFIG_REGISTRY stays.
|
||||
for (const key of Object.keys(env)) {
|
||||
if (key === 'npm_config_prefix' || key.startsWith('npm_config_') || key.startsWith('npm_package_')) {
|
||||
if (key.startsWith('npm_config_') || key.startsWith('npm_package_') || key.startsWith('pnpm_config_')) {
|
||||
delete env[key]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ export const resolveOutsideAsar = (specifier: string): string =>
|
||||
let getWindowsModule: Promise<GetWindowsModule | EnumerationFailure> | null = null
|
||||
|
||||
const loadGetWindows = (): Promise<GetWindowsModule | EnumerationFailure> => {
|
||||
// get-windows is an optionalDependency: `npm ci` can skip it when its native
|
||||
// get-windows is an optionalDependency: the install can skip it when its native
|
||||
// install fails, including Linux and Windows ARM64 where 9.3.0 has no
|
||||
// prebuilt. A missing module is therefore a normal state on those targets,
|
||||
// so the lazy import resolves to null instead of rejecting; enumeration then
|
||||
|
||||
+24
-24
@@ -15,34 +15,34 @@
|
||||
"node": ">=26"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "npm run clean:e2e && npm run clean:renderer && npm run clean:electron",
|
||||
"clean": "pnpm run clean:e2e && pnpm run clean:renderer && pnpm run clean:electron",
|
||||
"clean:e2e": "tsc --build tsconfig.e2e.json --clean",
|
||||
"clean:renderer": "tsc --build tsconfig.json --clean ",
|
||||
"clean:electron": "tsc --build tsconfig.electron.json --clean",
|
||||
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||
"dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev",
|
||||
"dev": "concurrently -k \"pnpm:dev:renderer\" \"pnpm:dev:electron\"",
|
||||
"dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 pnpm run dev",
|
||||
"dev:mock": "node ../../tests-js/scripts/mock-server.ts",
|
||||
"dev:renderer": "node scripts/assert-root-install.mjs && npm run clean:renderer && vite --host 127.0.0.1 --port 5174",
|
||||
"dev:renderer": "node scripts/assert-root-install.mjs && pnpm run clean:renderer && vite --host 127.0.0.1 --port 5174",
|
||||
"dev:electron": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"profile:main": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
||||
"profile:main:cpu": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"start": "npm run build && electron .",
|
||||
"start": "pnpm run build && electron .",
|
||||
"build:webapp": "node scripts/build-webapp.mjs",
|
||||
"build": "node scripts/build.mjs",
|
||||
"i18n:keys": "node scripts/i18n-keys.mjs",
|
||||
"smoke:browser-host": "node scripts/smoke-browser-host.mjs",
|
||||
"prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs",
|
||||
"builder": "node scripts/run-electron-builder.mjs",
|
||||
"pack": "npm run build && npm run builder -- --dir --publish never",
|
||||
"dist": "npm run build && npm run builder",
|
||||
"pack": "pnpm run build && pnpm run builder --dir --publish never",
|
||||
"dist": "pnpm run build && pnpm run builder",
|
||||
"payload": "node ../../scripts/build/python.mjs ../../scripts/bundles/stage.py --out build/agent-payload",
|
||||
"dist:bundled": "npm run payload && cross-env HERMES_DESKTOP_VARIANT=bundled npm run dist",
|
||||
"dist:mac": "npm run build && npm run builder -- --mac",
|
||||
"dist:mac:dmg": "npm run build && npm run builder -- --mac dmg",
|
||||
"dist:mac:zip": "npm run build && npm run builder -- --mac zip",
|
||||
"dist:win": "npm run build && npm run builder -- --win msix",
|
||||
"dist:win:msix": "npm run build && npm run builder -- --win msix",
|
||||
"dist:linux": "npm run build && npm run builder -- --linux AppImage deb rpm",
|
||||
"dist:bundled": "pnpm run payload && cross-env HERMES_DESKTOP_VARIANT=bundled pnpm run dist",
|
||||
"dist:mac": "pnpm run build && pnpm run builder --mac",
|
||||
"dist:mac:dmg": "pnpm run build && pnpm run builder --mac dmg",
|
||||
"dist:mac:zip": "pnpm run build && pnpm run builder --mac zip",
|
||||
"dist:win": "pnpm run build && pnpm run builder --win msix",
|
||||
"dist:win:msix": "pnpm run build && pnpm run builder --win msix",
|
||||
"dist:linux": "pnpm run build && pnpm run builder --linux AppImage deb rpm",
|
||||
"perf": "node scripts/perf/run.mjs",
|
||||
"update:shim": "bash ../../scripts/desktop-update/repro.sh shim",
|
||||
"update:shim:fail": "bash ../../scripts/desktop-update/repro.sh shim-fail",
|
||||
@@ -63,21 +63,21 @@
|
||||
"lint": "eslint src/ electron/",
|
||||
"lint:fix": "eslint src/ electron/ --fix",
|
||||
"fmt": "prettier --write 'src/**/*.{ts,tsx}' 'electron/**/*.ts' 'vite.config.ts'",
|
||||
"fix": "npm run lint:fix && npm run fmt",
|
||||
"fix": "pnpm run lint:fix && pnpm run fmt",
|
||||
"test:ui": "vitest run --project ui",
|
||||
"test:desktop:platforms": "vitest run --project electron",
|
||||
"test:find-in-page-native": "electron electron/find-in-page-native-fixture",
|
||||
"test:preview-activity-native": "electron electron/preview-activity-native-fixture",
|
||||
"test": "vitest run",
|
||||
"preview": "node scripts/assert-root-install.mjs && vite preview --host 127.0.0.1 --port 4174",
|
||||
"check:test:ui": "npm run test:ui",
|
||||
"check:test:desktop:platforms": "npm run test:desktop:platforms",
|
||||
"check:test:desktop:all": "npm run test:desktop:all",
|
||||
"check:lint": "npm run typecheck && npm run lint",
|
||||
"check": "npm run check:lint && npm run test:ui && npm run test:desktop:platforms && npm run test:desktop:all",
|
||||
"test:e2e": "npm run build && playwright test e2e/",
|
||||
"test:e2e:visual": "npm run build && WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- npx playwright test e2e/ --reporter=list",
|
||||
"test:e2e:update-snapshots": "npm run build && WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- npx playwright test e2e/ --reporter=list --update-snapshots",
|
||||
"check:test:ui": "pnpm run test:ui",
|
||||
"check:test:desktop:platforms": "pnpm run test:desktop:platforms",
|
||||
"check:test:desktop:all": "pnpm run test:desktop:all",
|
||||
"check:lint": "pnpm run typecheck && pnpm run lint",
|
||||
"check": "pnpm run check:lint && pnpm run test:ui && pnpm run test:desktop:platforms && pnpm run test:desktop:all",
|
||||
"test:e2e": "pnpm run build && playwright test e2e/",
|
||||
"test:e2e:visual": "pnpm run build && WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- pnpm exec playwright test e2e/ --reporter=list",
|
||||
"test:e2e:update-snapshots": "pnpm run build && WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- pnpm exec playwright test e2e/ --reporter=list --update-snapshots",
|
||||
"repro:short-session-hang": "node scripts/run-short-session-hang-repro.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -94,7 +94,7 @@
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
"@dnd-kit/sortable": "10.0.0",
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@hermes/shared": "file:../shared",
|
||||
"@hermes/shared": "workspace:*",
|
||||
"@icons-pack/react-simple-icons": "13.15.1",
|
||||
"@lezer/highlight": "1.2.5",
|
||||
"@nanostores/react": "1.1.0",
|
||||
|
||||
@@ -14,7 +14,7 @@ import { defineConfig, type ReporterDescription } from '@playwright/test'
|
||||
* artifacts for human review.
|
||||
*
|
||||
* To update baselines after an intentional UI change:
|
||||
* npx playwright test --update-snapshots
|
||||
* pnpm exec playwright test --update-snapshots
|
||||
*/
|
||||
const reporters: ReporterDescription[] = [
|
||||
['list'],
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Build-time guard: refuse to hand a half-built renderer to electron-builder.
|
||||
//
|
||||
// `npm run pack` / `npm run dist*` are `npm run build && npm run builder`.
|
||||
// `pnpm run pack` / `pnpm run dist*` are `pnpm run build && pnpm run builder`.
|
||||
// If the `build` step (tsc -b && vite build) fails but packaging proceeds
|
||||
// anyway — a stale checkout that fails typecheck, an interrupted vite build,
|
||||
// or npm not short-circuiting `&&` in some shells — electron-builder happily
|
||||
// or a shell not short-circuiting `&&` — electron-builder happily
|
||||
// packages an app with an empty or missing `dist/`. The result launches but
|
||||
// blank-pages with `ERR_FILE_NOT_FOUND` for dist/index.html, with no clue why.
|
||||
//
|
||||
// The desktop compiler (scripts/build/desktop.mjs) runs it on the scratch
|
||||
// product before publishing dist/, so every `npm run build` path inherits it
|
||||
// product before publishing dist/, so every `pnpm run build` path inherits it
|
||||
// once. It is deliberately not also a `postbuild` hook: that re-ran the same
|
||||
// check on the bytes just verified, doubling its cost in every update.
|
||||
// It fails loud and early instead of shipping a broken bundle.
|
||||
@@ -174,14 +174,14 @@ function main() {
|
||||
console.error(" completion. On Windows that is usually a stalled process creation")
|
||||
console.error(" (antivirus/EDR interference, memory pressure) and clears on a retry.")
|
||||
console.error(" If it repeats, check the node binary and any AV exclusion for the")
|
||||
console.error(` build tree, then re-run: cd ${desktopRoot} && npm run build\n`)
|
||||
console.error(` build tree, then re-run: cd ${desktopRoot} && pnpm run build\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.error(" The renderer bundle is missing or incomplete, so packaging")
|
||||
console.error(" would produce an app that launches to a blank page.")
|
||||
console.error(" Re-run the build and check the tsc/vite output above for the")
|
||||
console.error(" real failure, then package again:")
|
||||
console.error(` cd ${desktopRoot} && npm run build\n`)
|
||||
console.error(` cd ${desktopRoot} && pnpm run build\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Build-time guard: refuse to start a build the installed tree cannot finish.
|
||||
//
|
||||
// The desktop workspace's dependencies are hoisted to the repo-root
|
||||
// `node_modules`, so a root install that only covers *part* of the workspace
|
||||
// The desktop workspace's dependencies are linked into its own `node_modules`
|
||||
// by the root install, so an install that only covers *part* of the workspace
|
||||
// graph leaves this app importable-looking but unbuildable. The guard exists to
|
||||
// turn that into one actionable line ("run npm ci from the repo root") instead
|
||||
// of a failure deep inside vite.
|
||||
// turn that into one actionable line ("run pnpm install --frozen-lockfile from
|
||||
// the repo root") instead of a failure deep inside vite.
|
||||
//
|
||||
// It runs from `prebuild`, ahead of `npm run clean`, so a tree that cannot
|
||||
// It runs from `prebuild`, ahead of `pnpm run clean`, so a tree that cannot
|
||||
// build is rejected before the build starts deleting its own outputs. `build`
|
||||
// re-runs it for anyone invoking the build steps directly; the check is pure
|
||||
// filesystem lookups, so paying for it twice costs nothing.
|
||||
@@ -25,7 +25,7 @@ import { isMain } from "./utils.mjs"
|
||||
// the CSS transform fails before a single chunk is emitted.
|
||||
// electron — the runtime electron-builder packages; without it `pack`
|
||||
// cannot produce an unpacked app at all.
|
||||
// electron-builder — the packager `npm run builder` shells out to.
|
||||
// electron-builder — the packager `pnpm run builder` shells out to.
|
||||
//
|
||||
// Checking only `vite` (the original guard) passes a tree missing any of the
|
||||
// others, which is how an incomplete install reached `vite build` and died on
|
||||
@@ -41,7 +41,7 @@ import { isMain } from "./utils.mjs"
|
||||
// lands, so `checkRootInstall` unions the floor with the manifest's declared
|
||||
// `dependencies` + `devDependencies` — a partial install is refused whichever
|
||||
// package it happened to drop. `optionalDependencies` are excluded by design:
|
||||
// npm legitimately skips them (platform-gated natives like `get-windows`).
|
||||
// pnpm legitimately skips them (platform-gated natives like `get-windows`).
|
||||
const BUILD_CRITICAL_PACKAGES = ["vite", "katex", "electron", "electron-builder"]
|
||||
export { BUILD_CRITICAL_PACKAGES }
|
||||
|
||||
@@ -88,15 +88,15 @@ export function checkRootInstall(appDir, rootDir) {
|
||||
`the desktop build needs ${missing.join(", ")}, which the current install ` +
|
||||
`does not provide. A partial root install leaves the workspace looking ` +
|
||||
`present while the build cannot complete. Reinstall from the repo root: ` +
|
||||
`cd ${rootDir} && npm ci`
|
||||
`cd ${rootDir} && pnpm install --frozen-lockfile`
|
||||
}
|
||||
}
|
||||
|
||||
// `vite.config.ts` aliases react/react-dom to whatever this workspace resolves,
|
||||
// and React refuses to run when the two come from different installed copies
|
||||
// ("Minified React error #527" — it throws before the first paint, so the app
|
||||
// window stays blank). npm stays silent about the split because the hoisted
|
||||
// react still satisfies react-dom's caret peer range. Fail the build loudly
|
||||
// window stays blank). npm stayed silent about the split because the hoisted
|
||||
// react still satisfied react-dom's caret peer range. Fail the build loudly
|
||||
// instead of shipping a white screen.
|
||||
const requireFromApp = createRequire(join(appDir, "package.json"))
|
||||
const installedVersion = pkg =>
|
||||
@@ -113,7 +113,7 @@ export function checkRootInstall(appDir, rootDir) {
|
||||
// rather than an absent one. Report it as such instead of throwing.
|
||||
return {
|
||||
ok: false,
|
||||
error: `could not read the installed react/react-dom versions (${err.message}). Reinstall from the repo root: cd ${rootDir} && npm ci`
|
||||
error: `could not read the installed react/react-dom versions (${err.message}). Reinstall from the repo root: cd ${rootDir} && pnpm install --frozen-lockfile`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export function checkRootInstall(appDir, rootDir) {
|
||||
error:
|
||||
`react@${react} / react-dom@${reactDom} version mismatch — React would fail ` +
|
||||
`with error #527 and render a blank window. Pin both to the same version ` +
|
||||
`in ${join(appDir, "package.json")}, then reinstall: cd ${rootDir} && npm ci`
|
||||
`in ${join(appDir, "package.json")}, then reinstall: cd ${rootDir} && pnpm install --frozen-lockfile`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,21 +7,21 @@ import { test } from 'vitest'
|
||||
import { BUILD_CRITICAL_PACKAGES as BUILD_CRITICAL, checkRootInstall, requiredPackages } from '../scripts/assert-root-install.mjs'
|
||||
|
||||
// Build a throwaway repo shaped like this one: an app workspace whose
|
||||
// dependencies are hoisted to the repo root, which is what the guard walks.
|
||||
// `manifest` is merged into the app's package.json so tests can declare
|
||||
// dependencies pnpm links into its own node_modules, which is what the guard
|
||||
// walks. `manifest` is merged into the app's package.json so tests can declare
|
||||
// dependencies the guard is expected to read.
|
||||
function makeTree({ rootPackages = BUILD_CRITICAL, react = '19.2.7', reactDom = '19.2.7', manifest = {} } = {}) {
|
||||
function makeTree({ appPackages = BUILD_CRITICAL, react = '19.2.7', reactDom = '19.2.7', manifest = {} } = {}) {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-assert-root-'))
|
||||
const appDir = path.join(tempRoot, 'apps', 'desktop')
|
||||
fs.mkdirSync(appDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ name: 'desktop', ...manifest }), 'utf8')
|
||||
|
||||
const writePackage = (name, version) => {
|
||||
const dir = path.join(tempRoot, 'node_modules', name)
|
||||
const dir = path.join(appDir, 'node_modules', name)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name, version }), 'utf8')
|
||||
}
|
||||
for (const name of rootPackages) writePackage(name, '1.0.0')
|
||||
for (const name of appPackages) writePackage(name, '1.0.0')
|
||||
if (react !== null) writePackage('react', react)
|
||||
if (reactDom !== null) writePackage('react-dom', reactDom)
|
||||
|
||||
@@ -42,20 +42,20 @@ test('checkRootInstall passes on a complete root install', () => {
|
||||
// the build died on an unresolved `katex/dist/katex.min.css` (#86443).
|
||||
test('checkRootInstall fails when katex is missing but vite is present', () => {
|
||||
const { tempRoot, appDir } = makeTree({
|
||||
rootPackages: BUILD_CRITICAL.filter(name => name !== 'katex')
|
||||
appPackages: BUILD_CRITICAL.filter(name => name !== 'katex')
|
||||
})
|
||||
try {
|
||||
const result = checkRootInstall(appDir, tempRoot)
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(result.error, /katex/)
|
||||
assert.match(result.error, /npm ci/)
|
||||
assert.match(result.error, /pnpm install --frozen-lockfile/)
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('checkRootInstall reports every missing package at once', () => {
|
||||
const { tempRoot, appDir } = makeTree({ rootPackages: ['vite'] })
|
||||
const { tempRoot, appDir } = makeTree({ appPackages: ['vite'] })
|
||||
try {
|
||||
const result = checkRootInstall(appDir, tempRoot)
|
||||
assert.equal(result.ok, false)
|
||||
@@ -71,7 +71,7 @@ test('checkRootInstall reports every missing package at once', () => {
|
||||
// drop the case it already handled.
|
||||
test('checkRootInstall still fails when vite is missing', () => {
|
||||
const { tempRoot, appDir } = makeTree({
|
||||
rootPackages: BUILD_CRITICAL.filter(name => name !== 'vite')
|
||||
appPackages: BUILD_CRITICAL.filter(name => name !== 'vite')
|
||||
})
|
||||
try {
|
||||
const result = checkRootInstall(appDir, tempRoot)
|
||||
@@ -93,16 +93,16 @@ test('checkRootInstall fails on a react/react-dom version split', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// A package installed into the app's own node_modules rather than hoisted to the
|
||||
// root is still installed. The guard walks upward like Node does, so it must not
|
||||
// insist on the hoisted location.
|
||||
test('checkRootInstall accepts a package nested in the app workspace', () => {
|
||||
// A package the repo root installs is still installed for the app: Node's lookup
|
||||
// walks upward, and so does the guard, so it must not insist on the app's own
|
||||
// node_modules.
|
||||
test('checkRootInstall accepts a package installed at the repo root', () => {
|
||||
const { tempRoot, appDir } = makeTree({
|
||||
rootPackages: BUILD_CRITICAL.filter(name => name !== 'katex')
|
||||
appPackages: BUILD_CRITICAL.filter(name => name !== 'katex')
|
||||
})
|
||||
const nested = path.join(appDir, 'node_modules', 'katex')
|
||||
fs.mkdirSync(nested, { recursive: true })
|
||||
fs.writeFileSync(path.join(nested, 'package.json'), JSON.stringify({ name: 'katex' }), 'utf8')
|
||||
const hoisted = path.join(tempRoot, 'node_modules', 'katex')
|
||||
fs.mkdirSync(hoisted, { recursive: true })
|
||||
fs.writeFileSync(path.join(hoisted, 'package.json'), JSON.stringify({ name: 'katex' }), 'utf8')
|
||||
try {
|
||||
assert.deepEqual(checkRootInstall(appDir, tempRoot), { ok: true })
|
||||
} finally {
|
||||
@@ -118,7 +118,7 @@ test('checkRootInstall accepts a package nested in the app workspace', () => {
|
||||
test('checkRootInstall fails when a declared devDependency outside the floor is missing', () => {
|
||||
const { tempRoot, appDir } = makeTree({
|
||||
manifest: { devDependencies: { '@rolldown/plugin-babel': '1.0.0', esbuild: '1.0.0' } },
|
||||
rootPackages: [...BUILD_CRITICAL, 'esbuild']
|
||||
appPackages: [...BUILD_CRITICAL, 'esbuild']
|
||||
})
|
||||
try {
|
||||
const result = checkRootInstall(appDir, tempRoot)
|
||||
@@ -143,7 +143,7 @@ test('checkRootInstall fails when a declared runtime dependency is missing', ()
|
||||
}
|
||||
})
|
||||
|
||||
// npm skips optionalDependencies legitimately (platform-gated natives), so an
|
||||
// pnpm skips optionalDependencies legitimately (platform-gated natives), so an
|
||||
// absent optional package is not a partial install.
|
||||
test('checkRootInstall ignores missing optionalDependencies', () => {
|
||||
const { tempRoot, appDir } = makeTree({
|
||||
@@ -159,7 +159,7 @@ test('checkRootInstall ignores missing optionalDependencies', () => {
|
||||
test('checkRootInstall passes when every declared package is installed', () => {
|
||||
const { tempRoot, appDir } = makeTree({
|
||||
manifest: { dependencies: { '@scope/pkg': '1.0.0' }, devDependencies: { esbuild: '1.0.0' } },
|
||||
rootPackages: [...BUILD_CRITICAL, '@scope/pkg', 'esbuild']
|
||||
appPackages: [...BUILD_CRITICAL, '@scope/pkg', 'esbuild']
|
||||
})
|
||||
try {
|
||||
assert.deepEqual(checkRootInstall(appDir, tempRoot), { ok: true })
|
||||
@@ -171,7 +171,7 @@ test('checkRootInstall passes when every declared package is installed', () => {
|
||||
// The floor is unconditional: a manifest the guard cannot parse must not turn
|
||||
// the check off.
|
||||
test('checkRootInstall keeps the floor when the manifest is unreadable', () => {
|
||||
const { tempRoot, appDir } = makeTree({ rootPackages: ['vite'] })
|
||||
const { tempRoot, appDir } = makeTree({ appPackages: ['vite'] })
|
||||
fs.writeFileSync(path.join(appDir, 'package.json'), '{not json', 'utf8')
|
||||
try {
|
||||
assert.deepEqual(requiredPackages(appDir), [])
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* ---------------
|
||||
* electron-builder's final packaging step copies the stock `electron`
|
||||
* binary into `release/<platform>-unpacked/` and then renames it to the
|
||||
* product name (`Hermes`). If a PREVIOUS `npm run pack` was interrupted
|
||||
* product name (`Hermes`). If a PREVIOUS `pnpm run pack` was interrupted
|
||||
* (Ctrl-C, OOM kill, crash, full disk) the unpacked directory is left in a
|
||||
* corrupted partial state: it keeps the already-renamed `LICENSE.electron.txt`
|
||||
* and the Chromium payload (.pak/.so/icudtl.dat/chrome-sandbox) but is MISSING
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// npm's source-development composition for the browser-hosted renderer.
|
||||
// The source-development composition for the browser-hosted renderer.
|
||||
//
|
||||
// Dependencies are installed into a private workspace, never the checkout:
|
||||
// npm ci would prune the native Desktop install (node-pty, Electron), and
|
||||
// headless hosts have no toolchain for its lifecycle scripts. The workspace
|
||||
// mirrors exactly the inputs the Webapp receipt covers and keeps its
|
||||
// node_modules between builds, so npm ci reruns only when the lock changes.
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
||||
// a frozen install there would relink the native Desktop install (node-pty,
|
||||
// Electron), and headless hosts have no toolchain for its lifecycle scripts.
|
||||
// The workspace mirrors exactly the inputs the Webapp receipt covers and keeps
|
||||
// its node_modules between builds, so pnpm reinstalls only when the lock changes.
|
||||
import { copyFileSync, mkdirSync, rmSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { isMain, repoRoot } from '../../../scripts/build/frontend-common.mjs'
|
||||
@@ -13,7 +13,8 @@ import { sourceEntries } from '../../../scripts/build/freshness.mjs'
|
||||
import { prepareNodeDependencies } from '../../../scripts/build/node-deps.mjs'
|
||||
import { buildWebapp } from '../../../scripts/build/webapp.mjs'
|
||||
|
||||
/** Make the workspace hash exactly like the checkout for the Webapp product. */
|
||||
/** Make the workspace hash exactly like the checkout for the Webapp product. The filtered
|
||||
* frozen install needs no other workspace's manifest, so none is copied. */
|
||||
export function mirrorWebappInputs(source, workspace) {
|
||||
const wanted = new Map(sourceEntries(source, 'webapp').map(({ name, kind }) => [name, kind]))
|
||||
// Children before parents. Generated trees (node_modules) are never listed, so never removed.
|
||||
@@ -26,15 +27,6 @@ export function mirrorWebappInputs(source, workspace) {
|
||||
mkdirSync(kind === 'directory' ? target : dirname(target), { recursive: true })
|
||||
if (kind === 'file') copyFileSync(join(source, name), target)
|
||||
}
|
||||
// npm ci validates the whole locked workspace graph, so every workspace
|
||||
// manifest must exist even though only Desktop and shared are compiled.
|
||||
const lock = JSON.parse(readFileSync(join(source, 'package-lock.json'), 'utf8'))
|
||||
for (const entry of Object.values(lock.packages || {})) {
|
||||
const manifest = entry.link && join(entry.resolved, 'package.json')
|
||||
if (!manifest || !existsSync(join(source, manifest))) continue
|
||||
mkdirSync(dirname(join(workspace, manifest)), { recursive: true })
|
||||
copyFileSync(join(source, manifest), join(workspace, manifest))
|
||||
}
|
||||
}
|
||||
|
||||
/** Mirror the inputs into the private workspace and install its dependencies, without compiling.
|
||||
@@ -48,7 +40,7 @@ export function prepareWebappWorkspace({
|
||||
mirrorWebappInputs(source, workspace)
|
||||
prepare({
|
||||
source: workspace, workspaces: ['apps/desktop'], reuse: true, install,
|
||||
env: { ...process.env, npm_config_ignore_scripts: 'true' },
|
||||
env: { ...process.env, pnpm_config_ignore_scripts: 'true' },
|
||||
// The browser bundle never runs Electron, so its binary is not downloaded.
|
||||
electron: false,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// npm's source-development composition; the compiler consumes prepared inputs.
|
||||
// The source-development composition (`pnpm run build`); the compiler consumes prepared inputs.
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { cpSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
|
||||
@@ -8,8 +8,8 @@ import { withCompilerCache } from './compiler-cache.mjs'
|
||||
|
||||
function setup() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'compiler-cache-'))
|
||||
const lock = path.join(root, 'package-lock.json')
|
||||
fs.writeFileSync(lock, '{"v":1}')
|
||||
const lock = path.join(root, 'pnpm-lock.yaml')
|
||||
fs.writeFileSync(lock, "lockfileVersion: '9.0'")
|
||||
const calls = []
|
||||
const plugin = () => ({
|
||||
transform: {
|
||||
@@ -54,7 +54,7 @@ test('a toolchain or NODE_ENV change starts a new generation; a corrupt entry is
|
||||
await t.cached().transform.handler('<App/>', id)
|
||||
await t.cached({ env: { NODE_ENV: 'development' } }).transform.handler('<App/>', id)
|
||||
assert.equal(t.calls.length, 2)
|
||||
fs.writeFileSync(t.lock, '{"v":2}')
|
||||
fs.writeFileSync(t.lock, "lockfileVersion: '6.0'")
|
||||
await t.cached().transform.handler('<App/>', id)
|
||||
assert.equal(t.calls.length, 3)
|
||||
for (const name of t.entries()) fs.writeFileSync(path.join(t.root, 'cache', String(name)), '{torn')
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// node scripts/eval.mjs "document.title"
|
||||
// HERMES_DESKTOP_CDP_PORT=9333 node scripts/eval.mjs "document.title"
|
||||
//
|
||||
// Needs a renderer with a debugging port: launch `hgui` / `npm run dev` with
|
||||
// Needs a renderer with a debugging port: launch `hgui` / `pnpm run dev` with
|
||||
// HERMES_DESKTOP_CDP_PORT set (see electron/dev-cdp.ts).
|
||||
const port = Number(process.env.HERMES_DESKTOP_CDP_PORT || 9222)
|
||||
let targets
|
||||
@@ -13,7 +13,7 @@ try {
|
||||
} catch {
|
||||
console.error(
|
||||
`no renderer debugging port on 127.0.0.1:${port}. ` +
|
||||
'Dev-server runs (`npm run dev` / `hgui`) open one automatically — check the app is running, ' +
|
||||
'Dev-server runs (`pnpm run dev` / `hgui`) open one automatically — check the app is running, ' +
|
||||
'and that HERMES_DESKTOP_CDP_PORT is not set to "off" or another port.'
|
||||
)
|
||||
process.exit(1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Throwaway generator: deterministic fake star-map graphs → real share codes
|
||||
// (runs the actual encoder, so every string round-trips). Run with `npx tsx`.
|
||||
// (runs the actual encoder, so every string round-trips). Run with `pnpm exec tsx`.
|
||||
import { writeFileSync } from 'node:fs'
|
||||
|
||||
import type { StarmapEdge, StarmapGraph, StarmapMemoryCard, StarmapNode } from '../src/types/hermes'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// `locales/_keys.desktop.json` (repo root, committed). This script derives that
|
||||
// file from `src/i18n/en.ts` — every leaf's dotted path, sorted; function-
|
||||
// valued entries are keys too (a pack overrides them with a positional
|
||||
// `{0}`/`{1}` string). Run via `npm run i18n:keys` and commit the result (`--check` verifies);
|
||||
// `{0}`/`{1}` string). Run via `pnpm run i18n:keys` and commit the result (`--check` verifies);
|
||||
// the build never writes it — a dirty checkout would break `hermes update`.
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
@@ -84,7 +84,7 @@ export async function emitDesktopKeys({ source = repoRoot, out, check = false }
|
||||
// A missing file is drift too.
|
||||
}
|
||||
if (current !== rendered) {
|
||||
throw new Error(`${out} is out of date with src/i18n/en.ts — run \`npm run i18n:keys\` in apps/desktop and commit it`)
|
||||
throw new Error(`${out} is out of date with src/i18n/en.ts — run \`pnpm run i18n:keys\` in apps/desktop and commit it`)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ test('flattenKeys lists every leaf as a sorted dotted path, functions included,
|
||||
assert.equal(renderKeysFile(['a']), `{\n "surface": "${SURFACE}",\n "keys": [\n "a"\n ]\n}\n`)
|
||||
})
|
||||
|
||||
test('the committed locales/_keys.desktop.json matches the English catalog (run `npm run i18n:keys`)', async () => {
|
||||
test('the committed locales/_keys.desktop.json matches the English catalog (run `pnpm run i18n:keys`)', async () => {
|
||||
const out = join(repoRoot, 'locales', `_keys.${SURFACE}.json`)
|
||||
const committed = JSON.parse(readFileSync(out, 'utf8'))
|
||||
assert.equal(committed.surface, SURFACE)
|
||||
|
||||
@@ -10,20 +10,20 @@ client, arg parsing, stats, and output (and never had a baseline).
|
||||
```bash
|
||||
# Isolated instance (recommended) — no running app or LLM credits needed.
|
||||
# Its own --user-data-dir + HERMES_HOME means it never collides with `hgui`.
|
||||
npm run perf -- --spawn
|
||||
pnpm run perf --spawn
|
||||
|
||||
# Or: launch an isolated instance once, attach repeatedly (faster iteration).
|
||||
npm run perf:serve # leaves an instance on :9222
|
||||
npm run perf # attaches, runs the CI suite, gates on baseline
|
||||
pnpm run perf:serve # leaves an instance on :9222
|
||||
pnpm run perf # attaches, runs the CI suite, gates on baseline
|
||||
|
||||
# One scenario, with a CPU profile:
|
||||
npm run perf -- stream --cpuprofile --tokens 800
|
||||
pnpm run perf stream --cpuprofile --tokens 800
|
||||
|
||||
# Representative PRODUCTION numbers (minified React, not the ~3x-slower dev build):
|
||||
npm run perf -- cold-start stream keystroke transcript --spawn --prod
|
||||
pnpm run perf cold-start stream keystroke transcript --spawn --prod
|
||||
|
||||
# Re-capture the baseline on your reference device, then commit baseline.json:
|
||||
npm run perf -- cold-start stream keystroke transcript --spawn --prod --update-baseline
|
||||
pnpm run perf cold-start stream keystroke transcript --spawn --prod --update-baseline
|
||||
```
|
||||
|
||||
## Profiling an existing workspace
|
||||
|
||||
@@ -215,7 +215,7 @@ export async function requireDriver(cdp) {
|
||||
if (!ok) {
|
||||
throw new Error(
|
||||
'__PERF_DRIVE__ not on window. The perf harness needs a DEV renderer ' +
|
||||
'(perf-probe.tsx is excluded from production builds). Launch with `npm run perf:serve`.'
|
||||
'(perf-probe.tsx is excluded from production builds). Launch with `pnpm run perf:serve`.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loaded 100 of 304 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user