feat: add ecc installer - #216
Merged
Merged
Conversation
The development ecc-wrapper.sh cd'd into the ecc repository before running `uv run ecc`, so relative path arguments (e.g. --project gcd) resolved against the repository instead of the caller's directory. Pass the project explicitly with `uv run --project <repo>/ecc` (and `nix develop <repo>/ecc --command` on the Nix branch) so the caller's working directory is preserved. This uniformly affects the sidecar shim, the integrated-terminal dev shim, and the future dev ecos-ecc shim.
Introduce the shared surface for the upcoming ECC bundle installer: 'cli-installer:*' IPC channels plus a progress event channel, the CliInstallState/progress contract (extended with 'unsupported' and 'dev-wrapper' statuses for non-Linux hosts and development mode), and the pinned EXPECTED_ECC_BUNDLE_VERSION / ECC_BUNDLE_RESOURCE_ID constants the installer and the future registry entry share.
Add two seams the ECC bundle installer needs without touching the existing install flows or manifest schema: - downloadRegistryAssetToDirectory(): resolves a registry tool entry, downloads with the standard retry policy, verifies size and sha256, and extracts (honoring strip_prefix) into a caller-provided directory. Unlike installResource it writes no manifest entry, runs no tool health policy, and detects no executables, so consumers that own their on-disk layout (the ECC bundle home) can reuse the machinery. - onManifestChanged(): a subscribe/unsubscribe hook fired whenever the active-state manifest changes, so the installer can regenerate the shared env file after Resource Manager installs and uninstalls.
resolveEccExecutable() and createEccRuntimeEnv() now fall back to <datadir>/ecos-studio/ecc-runtime/current/binaries when the packaged resources do not embed the ECC bundle (slim packages), ahead of the 'not resolved' state. The bundle home is materialized by the upcoming CLI installer; the current symlink only ever points at complete installs. LD_LIBRARY_PATH keeps pointing into the active _internal/ ecc_tools_bin/lib directory whichever source wins. Development mode is unchanged.
Address review findings on the registry download API and runtime env: - Stage external downloads at a unique per-call archive path next to the destination instead of the shared resumable .part location, and serialize them behind a service-level lock, so concurrent external downloads (or overlapping tool installs) cannot race on staging. - Publish a terminal 'cancelled' progress event when the download signal aborts, matching installTool's behavior. - Fire onManifestChanged for managed PDK installs, imports, and reference removals, which commit outside mutateManifest. - Observe async manifest listeners' rejections and widen the listener type to allow Promise-returning callbacks. - Require the registry version in RegistryAssetDownloadRequest; the pinned ECC bundle contract has no latest fallback. - Treat an empty XDG_DATA_HOME as unset in the runtime env data-home resolution, matching the Resource Manager semantics. - Document the per-status invariants on CliInstallState.
Owns the host bundle-home layout
<datadir>/ecos-studio/ecc-runtime/<version>-<sha8>/{binaries,env,install.json}
with an atomically switched 'current' symlink (rename over a temporary
link), the generated env file, and the ~/.local/bin/ecos-ecc shim:
- ensureBundle() copies the packaged resources/binaries bundle when
embedded, otherwise downloads the pinned registry asset through the
ResourceManagerService seam, validates the layout, runs an
'ecc --version' self-check with the fully merged runtime env, and
only then publishes install.json and switches 'current'. Failed
attempts discard their staging directory and leave 'current' intact.
- The env file restricts createEccRuntimeEnv + createRuntimeEnv to the
ECC-relevant subset (tool PATH, LD_LIBRARY_PATH, OSS CAD/RISCV/ECOS_
vars; ECOS_ELECTRON_* and session vars excluded) and writes PATH and
LD_LIBRARY_PATH as prepend expressions that reference the active
version directory. It is regenerated on manifest (tool) changes via
a debounced onManifestChanged subscription.
- checkSyncOnStartup() cleans stale .tmp-* staging dirs and compares
install.json against the expected identity (pinned version plus the
packaged ecc hash for 'bundled', the registry sha256 for
'downloaded'), reinstalling in the background on drift; registry
failures keep the current install.
- Development mode installs a shim that execs the repository wrapper
(<userData>/runtime-bin/ecc) and sources a dev env file; bundle
install, download, and drift sync are disabled. Non-Linux platforms
report an 'unsupported' state.
resourceManagerService notify calls added in the previous commit for
PDK inventory paths are reverted: the runtime env is derived from the
resource manifest tool entries only, so PDK inventory changes cannot
affect the env file and the hook stays scoped to manifest commits.
… lock New electron/main/cliEntry.ts parses '--cli <command> [args...]' from the first user argument, applies the headless ozone platform hint when no DISPLAY/WAYLAND_DISPLAY exists, and execs the ECC binary with stdio inherited, SIGINT/SIGTERM forwarding, and the child's exit code (signals map to 128+n). Only 'ecc' is supported; anything else prints usage and exits non-zero. A missing ECC executable prints GUI-once download guidance and exits non-zero; non-Linux platforms warn and exit non-zero. electron/main/index.ts parses the invocation before requesting the single-instance lock, so a --cli launch is always an independent process even while the GUI runs, and resolves the executable and runtime env exactly as the ECC RPC sidecar does (createEccRuntimeEnv + ResourceManagerService.createRuntimeEnv). The module stays free of Electron imports for unit testing.
Address review findings on CliInstallerService: - Drift detection now compares the active install against the source the running package actually provides (embedded bundle hash when present, otherwise the registry sha256), so slim<->fat upgrades re-acquire from the source GUI resolution prefers instead of silently keeping a downloaded bundle. - A same-identity reinstall now validates the existing version directory (layout + receipt + passing self-check) and replaces a damaged or self-check-failed copy with the fresh staging via a backup-then-rename dance, so transient self-check failures can be repaired and corruption fixed. - status() only treats a complete install as active and reports an incomplete 'current' target as failed with a repair hint; startup drift sync re-acquires incomplete installs. - installShim writes to a temp file, chmods explicitly, and renames atomically so a partial write cannot damage a working shim. - The env file uses a finite allowlist of ECC runtime variables and single-quotes the PATH/LD_LIBRARY_PATH prepend segments so arbitrary paths stay literal while the caller's values still expand. - Dev shim install fails when repository discovery finds no wrapper, and the shim (like the rest of the installer) is Linux-only. - Startup cleanup also removes stale .current-* links and .old-* backups.
- getDesktopServices() constructs CliInstallerService; GUI startup runs
checkSyncOnStartup() and acquires the bundle on first use when the
package embeds no ECC binaries (slim packages), with progress logged.
- The main ECC sidecar now resolves its executable through resolveLaunch
on every start, so a bundle downloaded or refreshed by the installer is
picked up without an app restart.
- registerIpc exposes cli-installer:get-status/install/uninstall with
progress forwarded over the cli-installer:progress event channel;
preload exposes desktopApi.cliInstaller accordingly and DesktopApi
gains the required cliInstaller member (renderer test doubles updated).
- parseCliInvocation now distinguishes a non-CLI launch from an invalid
one: bare --cli produces an empty command handled by usage/exit 2, and
only an electron default-app entry prefix ('.', *.js/*.mjs/*.cjs/*.asar)
is skipped, so '[exe, workspace, --cli, ecc]' stays a GUI launch.
- The CLI whenReady chain gains a rejection handler that logs and exits 1.
- Repair reinstalls activate a unique sibling directory and delete the
damaged one only after 'current' moved off it, so 'current' never
dangles and crash windows cannot lose the rollback copy; readInstallRecord
also requires the env file, and the shim temp file is unique with an
explicit chmod.
- runtimeEnvProvider rebuilds createEccRuntimeEnv on every sidecar env resolution, so a slim package's downloaded bundle contributes fresh PATH and LD_LIBRARY_PATH entries instead of the empty startup env. - The env file is written by finalizeStagedBundle against the final directory name, so repaired installs (unique sibling directory) no longer source stale entries pointing at the deleted canonical dir; same-identity keeps refresh their env file too. - parseCliInvocation treats only an existing regular file (or '.') as an electron default-app entry prefix, so workspace directories whose names end in .js stay GUI launches. - uninstall refuses to run while an install is in flight and installs refuse to start during an uninstall, preventing resurrecting or partially torn-down installs via concurrent IPC.
New CliInstallerCard component (self-contained status/actions) exposes the host CLI installer in the GUI: - Mounted on the Resource Manager (/tools) page as a permanent 'Command line tools' section: state pill, version/bundle/shim metadata, install-progress bar driven by cli-installer:progress events, install/reinstall/uninstall actions, and error surfaces for failures, self-check problems, and unsupported platforms. - Mounted on the home view in guidance mode (only rendered while the CLI is not ready) so a slim package's first launch shows the core component download with retry, per the installer design. - New renderer api module wraps the desktop bridge surface; existing ECOSView/PluginToolsView tests get the card stubbed or mocked.
check-version.py now compares the shared EXPECTED_ECC_BUNDLE_VERSION constant against ecc/pyproject.toml, so an ecc submodule bump without the matching registry-pin update fails the release version gate instead of silently downloading a mismatched runtime.
Address review findings on the Command line tools card and IPC: - Progress events now drive the card regardless of origin: startup acquisition (started before the window opens) shows the progress bar in the guidance card, and terminal events clear it and refresh the status, so first-launch installs can no longer stick at 'Installing'. - A user-initiated install optimistically switches to 'installing', shows the bar, and disables the buttons (also for externally observed installs), preventing duplicate IPC installs. - The install IPC handler no longer swallows installShim() failures: the shim error (with manual-shim remediation) is surfaced on the returned ready state and rendered by the card. - Development mode: the shim Install action appears only while the shim is missing, Uninstall only while it exists, and guidance-mode visibility includes the missing-shim case. - The progress subscription is unsubscribed on unmount, so route navigation no longer accumulates listeners; refreshStatus keeps the last known state when the bridge fails; uninstall errors surface. - The progressbar gets an accessible name. - The ECOSView test harness returns the card stub directly so the transpiled CJS interop resolves it.
Address the final full-branch review findings: - Installer progress is now broadcast to every window by the startup wiring, so first-use downloads started before the window opens (and their terminal events) reach the renderer guidance card; the per-request forwarding in the install handler is removed, removing duplicate events. - status() no longer reports 'ready' when the ecos-ecc shim is missing: the host command does not exist, so the state is 'failed' with a reinstall hint, even after restarts. - Generated env files are written to a sibling temp file and renamed atomically, so a concurrent ecos-ecc invocation never sources a truncated or half-written file. - CLI signal forwarding keeps SIGINT and SIGTERM distinct (a shared handler forwarded SIGINT as the default SIGTERM, turning Ctrl-C exits into 143); the signal test now emits through the real signal argument path. - The 1,024-line installer service is split per the scoped file-size guidance: cliInstallerArtifacts.ts (POSIX quoting, env-file allowlist/content, shim script, receipt parsing), cliSelfCheck.ts (spawned self-check + spawn seams), cliInstallerEnv.ts (runtime-env merging and atomic env writing), leaving the service as orchestration.
- The install IPC handler no longer force-runs ensureBundle(): in
development mode it only (re)creates the repository-wrapper shim, so
the dev Install action works instead of always failing.
- ensureBundle() no longer touches the shim: background drift repair
refreshes only the bundle (per the design's upgrade criterion), and
shim installation happens explicitly on user installs and first-use
setup (startup task now installs the shim after acquisition).
- installShim() and uninstall() never touch a pre-existing command at
~/.local/bin/ecos-ecc that lacks the ECOS Studio marker, and status()
treats foreign files as 'shim missing' rather than installed.
- Resource-manager download events are remapped ('done' -> intermediate
'acquired'; terminal phases suppressed) so the renderer sees one
continuous install with a single terminal event; card buttons also
disable while a background drift install is active.
- The generated development runtime shim single-quotes the wrapper
path, matching the outer shim hardening.
- Fix the stale CliInstallState 'failed' invariant doc and the
duplicated 'Detected versions' print in check-version.py.
- Split the installer: acquisition pipeline -> cliInstallerAcquisition,
self-check/spawn seam -> cliSelfCheck, bringing every new production
file under the scoped size guidance.
… and repair atomicity - ensureBundle() gains an installShim option: user-initiated installs and first-use setup run the shim install inside the pipeline so the terminal 'done' event covers a fully usable CLI, and a shim failure publishes the terminal error with the manual-shim remediation, which status() then preserves; background drift calls ensureBundle() without the option and never touches the shim. - The install IPC handler takes the dev-wrapper branch before any bundle work and no longer force-runs ensureBundle in development mode; for packaged installs a shim failure is reflected on the returned failed status instead of being dropped. - The acquisition finalize step no longer switches 'current' itself: the pipeline performs exactly one switch and deletes the superseded directory afterwards, removing the double-switch window. - readInstallRecord now lives only in cliInstallerArtifacts; the acquisition pipeline imports the authoritative implementation. - CliInstallerCard disables its actions while a background drift install is in flight (tested), matching the activeJob state.
…ent-target resolution Address the third acceptance-review round: - installShim() detects existing shim-path entries with lstat so a foreign dangling symlink is refused instead of silently replaced. - ensureBundle() deduplication honors a requested installShim: a caller overlapping an in-flight install gets the shim installed once the bundle work settles, instead of the option being dropped. - resolveCurrentVersionDir() rejects 'current' targets that resolve outside the bundle home or are not directories, so a tampered link cannot redirect env-file writes. - Regression tests: foreign dangling shim symlink refusal, shim install honored under concurrent install deduplication, escaped 'current' target ignored.
…heckout Address the fourth acceptance-review round: - resolveCurrentVersionDir() uses a shared physical containment helper (realpath-based) so chained intermediate symlinks can no longer point 'current' outside the bundle home; the GUI/--cli runtime resolver applies the same validation when picking the bundle-home binary, and a two-hop symlink chain is covered by tests on both sides. - The release version gate checks out submodules, since EXPECTED_ECC_BUNDLE_VERSION is compared against the pinned ecc/pyproject.toml.
…tion Address the final review finding: when ensureBundle() deduplicates onto an in-flight install, a requested installShim is now chained into the tracked ensurePromise (instead of running outside it), so uninstall guards still apply, shim failures publish terminal error progress with the manual-shim remediation and are recorded for status(), and success publishes the terminal done event. The unused fixture in the concurrency test is removed.
…e sidecar readiness Address the fifth acceptance-review round: - Development-mode shim installation now runs inside the tracked mutation transaction: installShim() is serialized (deduplicated) and refuses to run during an uninstall, and uninstall() also guards against an in-flight shim install, closing the recreate-after-uninstall race between windows. - finalizeStagedBundle() validates an existing version directory with the same physical-containment policy as 'current' (lstat/realpath), so a symlinked version path cannot redirect env writes outside the bundle home; the pipeline cleans up placed-but-unactivated directories when activation fails. - Sidecar resolveLaunch() no longer falls back to a PATH 'ecc' when the bundle is unresolved: it raises a clear 'core component is not ready' error so a workspace opened during first-use acquisition waits for the download instead of launching the wrong executable. Spec note (docs/superpowers/specs/2026-08-26-ecc-bundle-installer-design.md, untracked): PDK roots are workspace-bound state and intentionally excluded from the shared host env file, which is derived from manifest tool entries only.
Node 23 (the version CI pins via ecos/gui/.nvmrc) rejects rmSync without recursive on a symlink to a directory with ERR_FS_EISDIR, while Node 24 unlinks it. Pass recursive + force: rm still unlinks the symlink itself and leaves the target directory untouched.
The home page card hid itself once ecos-ecc became ready, so reinstall and uninstall required navigating to Plugin Tools. Keep the card mounted in every state so those actions stay one click away, and drop the now-unused showOnlyWhenActionNeeded guidance mode.
Add an 'external' install source for user-provided ECC runtimes that ECOS Studio uses read-only, and a non-blocking warning field so version drift against the pinned bundle can be surfaced without failing the install state. All existing producers report warning: null.
ECOS_ECC_BIN_DIR (or the explicit externalEccBinDir option, the seam a future settings page will feed) points at a directory containing an executable ecc and wins every other resolution — packaged binaries, the managed bundle home, and the development wrapper alike. Invalid values (relative path, missing executable) are ignored; an invalid explicit option disables the override without falling back to the environment. Two pre-existing packaged-mode tests are made hermetic so a real bundle home on the host no longer leaks into their PATH and LD_LIBRARY_PATH assertions.
accessSync(X_OK) succeeds for a searchable directory on POSIX, so an override whose ecc entry was a directory passed validation, suppressed the packaged/bundle fallback, and only failed at spawn time. Require a regular file so malformed overrides are ignored instead.
When an external ECC bin directory is configured (ECOS_ECC_BIN_DIR via the composition root, or the injected option a settings page will feed): - status() self-checks the external executable live and reports source: 'external', a best-effort parsed version, and a non-blocking warning on version drift from the pinned bundle version - ensureBundle() skips acquisition entirely; it only regenerates the shared env file into our own data dir and optionally writes the shim - the shim sources <dataDir>/env and execs the external ecc; nothing is ever written into the external tree - uninstall() removes only our shim and the generated env file, leaving the external tree and any managed bundle intact - startup drift repair is skipped for the unused managed bundle
The card now says when the runtime is external (used read-only, never modified by ECOS Studio), hints at installing the shim when none exists, and renders the non-blocking version-drift warning separately from the blocking error line. There is deliberately no path-editing UI: the override stays env-var-only until the settings page exists.
Cover the bin-dir contract, read-only semantics, version-drift warning, and the ecc-installer.sh install path as the canonical example.
Address codex review findings on the external runtime override: - a shim only counts as installed when its content matches what the current mode would generate, so a managed shim left behind after switching to an external override (or the reverse) no longer silently executes the wrong ECC; status reports it as missing or stale with the existing reinstall remediation - a recorded external shim-install failure stays visible as the status error even when the runtime self-check passes, instead of being replaced by a bare Ready state - development mode with an external override now also derives LD_LIBRARY_PATH from the bundle's _internal libraries, matching the packaged branch
The packaging pipeline no longer stages ECC into resources/binaries; packages acquire the pinned bundle from the registry on first run through the CLI installer's existing slim path. This drops the ECC build/download step from build-binaries.sh (its output had no other consumer), removes the packaged-ECC after-pack validation, and keeps Chip Viewer and the bundled Agent staged and validated. Release precondition: the registry must serve the pinned EXPECTED_ECC_BUNDLE_VERSION or first-run acquisition fails for every user. Verified with make build on Linux x86_64: the AppImage packages with chip-viewer-native and the agent but no ecc, and after-pack passes. The deb target could not be exercised locally (fpm's vendored ruby does not run on NixOS); it is unaffected by this change and remains covered by CI.
Codex review follow-up: doctor and the AppImage smoke check still required the packaged ECC payload that slim builds intentionally omit. doctor now requires only the staged chip-viewer-native (the ECC CLI check stays a host-level warning), and the AppImage smoke check validates the payload that remains packaged. The smoke check was run against the slim AppImage produced by make build and passes.
The companion test asserted dry-run fragments for the ecc_tools_bin and DreamPlace payload entries that slim builds no longer require, so it failed deterministically against the updated smoke script.
Emin017
force-pushed
the
emin/add-ecc-installer
branch
from
September 10, 2026 13:10
fabf9d3 to
acfd8e6
Compare
The after-pack wrapper prepended --no-sandbox when the setuid helper was unusable, so Electron saw argv[1] as --no-sandbox and parseCliInvocation never matched --cli: an ECC pass-through launch silently booted the GUI on exactly the machines the fallback exists for. Export ELECTRON_DISABLE_SANDBOX=1 instead to keep user arguments in place with identical sandbox behavior.
The ECC-not-ready message printed by the --cli pass-through looked like normal output; it now leads with an Error: label, colored red on interactive terminals and kept plain when stderr is piped or NO_COLOR is set.
Make cliErrorPrefix take env and TTY state explicitly so the red/plain branches are unit-testable, and add the interactive, piped, and NO_COLOR cases flagged by review.
Integrated shell sessions inherited the managed ECC bin dir, but the ecos-ecc shim only resolved when the desktop session already had ~/.local/bin on PATH. ShellPtyService now prepends the user bin dir (Linux only, default ~/.local/bin, opt-out via userBinDir: null) so the shim works in every integrated terminal; host shell rc files stay untouched.
Skipping the prepend when ~/.local/bin appeared anywhere in PATH let an earlier directory shadow the ecos-ecc shim; review flagged the gap. Existing entries are now moved to the front instead, and an empty PATH no longer gains a trailing separator.
Emin017
marked this pull request as ready for review
September 11, 2026 06:37
The registry now publishes tool:ecc at 0.1.0-alpha.12 and the CLI bundle acquisition resolves the requested version by exact match, so the pinned expectation must track the published version.
Signed-off-by: Emin <me@emin.chat>
KoEkko
approved these changes
Sep 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Scope
Select the areas touched by this PR:
ecos/gui, including renderer, Electron, and shared packages.Validation
List the commands you ran. Mark checks that are not applicable as N/A.
cd ecos/gui && pnpm run typecheckcd ecos/gui && pnpm run testcd ecos/gui && pnpm run buildmake buildmake demo-gcdmake demo-retrosoccd ecos/gui && pnpm run devSkipped checks and reason:
Screenshots or Recordings
Required for visible GUI changes.
Release, Packaging, and Runtime Impact
Notes:
Checklist