Skip to content

feat: add ecc installer - #216

Merged
KoEkko merged 41 commits into
mainfrom
emin/add-ecc-installer
Sep 12, 2026
Merged

KoEkko merged 41 commits into
mainfrom
emin/add-ecc-installer

Conversation

@Emin017

@Emin017 Emin017 commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

Scope

Select the areas touched by this PR:

  • GUI - desktop UI/runtime changes in ecos/gui, including renderer, Electron, and shared packages.
  • ECC - ECC submodule updates or ECOS Studio integration with the ECC CLI/runtime.
  • Resource management - resource registry, downloads, installation, manifests, PDKs, or tool assets.
  • Build, packaging, Nix, or release workflow - build inputs, AppImage packaging, or release metadata.
  • CI - GitHub Actions workflows, reusable actions, triggers, path filters, or automated checks.
  • Documentation only - README, guides, templates, or docs with no runtime behavior change.

Validation

List the commands you ran. Mark checks that are not applicable as N/A.

  • cd ecos/gui && pnpm run typecheck
  • cd ecos/gui && pnpm run test
  • cd ecos/gui && pnpm run build
  • make build
  • make demo-gcd
  • make demo-retrosoc
  • Manual GUI smoke: cd ecos/gui && pnpm run dev
  • Other:

Skipped checks and reason:

Screenshots or Recordings

Required for visible GUI changes.

Release, Packaging, and Runtime Impact

  • No release, packaging, or runtime impact
  • Version metadata changed
  • AppImage or Electron packaging changed
  • ECC CLI runtime resources changed
  • OSS CAD Suite, PDK, resource download, or installer behavior changed
  • Submodule gitlink changed

Notes:

Checklist

  • I kept the change scoped to the affected component.
  • I updated docs or user-facing text where behavior changed.
  • I included lockfile changes for dependency updates.
  • I documented intentional submodule updates.
  • I did not include local caches, virtual environments, or generated build outputs.
  • I explained any skipped validation and remaining risk.

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.
@Emin017
Emin017 marked this pull request as draft September 9, 2026 07:30
@Emin017 Emin017 added this to the 0.1.0-alpha.10 milestone Sep 9, 2026
@Emin017 Emin017 added the enhancement New feature or request label Sep 9, 2026
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
Emin017 force-pushed the emin/add-ecc-installer branch from fabf9d3 to acfd8e6 Compare September 10, 2026 13:10
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
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
KoEkko merged commit 743d38f into main Sep 12, 2026
7 checks passed
@KoEkko
KoEkko deleted the emin/add-ecc-installer branch September 12, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants