A packaged Windows build reported "Microsoft Store returned invalid
JSON" on every update check. The Store checker, the App Installer
checker and the relaunch waiter were resolved under the payload's repo
snapshot (<payload>/hermes-agent/apps/desktop/scripts/), but
INERT_SNAPSHOT_DIRS has excluded apps/ and scripts/ from that snapshot
since f6713f2762. The installed 2026.6523.3313.0 package has no apps/
directory at all. The payload python then failed to open the script,
exited 2 with empty stdout, and parseResult reported invalid JSON.
The two checkers are python that runs under the payload interpreter, so
they belong with the core code, like the other hermes_cli helpers the
desktop runs with -m:
- apps/desktop/scripts/check-store-update.py becomes
hermes_cli/windows_store_update.py and check-appinstaller-update.py
becomes hermes_cli/windows_appinstaller_update.py (git mv).
- The runner spawns `python -P -m <module>` with PYTHONPATH set to the
bundled core code, then the venv dependencies: the order the bin
launchers give sys.path. -P keeps the working directory off sys.path.
- The relaunch waiter is PowerShell run outside the package, not core
code. It ships as an app resource (win.extraResources) and resolves
from process.resourcesPath.
The Python tests now import the modules, and a real `python -P -m`
subprocess test proves a checker failure still reaches the desktop as
JSON. A vitest checks the packaging config ships the waiter.
69 lines
2.6 KiB
TypeScript
69 lines
2.6 KiB
TypeScript
/** Electron consumes the launch contract completed by the PM bundle builder. */
|
|
import { createHash } from 'node:crypto'
|
|
import path from 'node:path'
|
|
|
|
import { INSTALL_STAMP, type InstallStamp, type PayloadRuntime } from './install-stamp'
|
|
|
|
export interface PayloadInfo extends PayloadRuntime {
|
|
root: string
|
|
shim: string
|
|
}
|
|
|
|
export function bundledPayload(
|
|
resourcesPath: string,
|
|
stamp: Readonly<InstallStamp> | null = INSTALL_STAMP
|
|
): PayloadInfo | null {
|
|
if (stamp?.payload !== 'bundled') {
|
|
return null
|
|
}
|
|
|
|
// The builder validates these paths before baking the stamp. There is no
|
|
// discovery, filesystem validation or alternative payload at runtime.
|
|
const runtime = stamp.runtime!
|
|
const root = path.join(resourcesPath, 'agent-payload')
|
|
|
|
const commands = Object.fromEntries(
|
|
Object.entries(runtime.commands).map(([name, relative]) => [name, path.join(root, relative)])
|
|
)
|
|
|
|
return {
|
|
root,
|
|
repoDir: path.join(root, runtime.repoDir),
|
|
toolsDir: path.join(root, runtime.toolsDir),
|
|
storePython: path.join(root, runtime.storePython),
|
|
sitePackages: path.join(root, runtime.sitePackages),
|
|
commands,
|
|
shim: commands.hermes
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PYTHONPATH for running a bundled core module with the payload python: the
|
|
* bundled core code first, then the venv's dependencies. This is the order the
|
|
* bin/ launchers give sys.path (scripts/build/launcher_wrapper.py).
|
|
*/
|
|
export function payloadPythonPath(payload: Pick<PayloadInfo, 'repoDir' | 'sitePackages'>): string {
|
|
return [payload.repoDir, payload.sitePackages].join(path.delimiter)
|
|
}
|
|
|
|
// ─── update channel ─────────────────────────────────────────────────────────
|
|
//
|
|
// The CLI owns the channel records; Electron only reads the install id for
|
|
// `update.installs.<sha16>/` bookkeeping. Channel resolution itself lives in
|
|
// hermes_cli/update_channel.py — main.ts keys canary/stable off the baked
|
|
// install stamp tag directly.
|
|
|
|
export type UpdateChannel = 'stable' | 'main' | 'canary'
|
|
|
|
/**
|
|
* The install id of the tree at `root`: sha16 of the canonical PATH,
|
|
* byte-identical to Python's install id (sha256 of the resolved root,
|
|
* first 16 hex chars — `boot_bootstrap._install_key` /
|
|
* `update_channel._install_key_sha16`). Path-derived so it survives
|
|
* artifact replacement at the same location; the same key names
|
|
* `installs/<sha16>/`.
|
|
*/
|
|
export function installIdForRoot(root: string, canonicalize: (p: string) => string = p => p): string {
|
|
return createHash('sha256').update(canonicalize(root), 'utf8').digest('hex').slice(0, 16)
|
|
}
|