Skip to content

Self-provisioning developer rebuilds across supported hosts #1016

Description

@jeonghun-jj-lee

Self-provisioning developer rebuilds across supported hosts

Important

Problem — Developer rebuild buttons assume ambient bun, a paired opencode fork checkout, macOS paths, and a live-file copy deployment. A fresh Linux or WSL user cannot even start a rebuild, and partial failures leave the installed extension in a mixed state. Rebuild from Main must also obey the promoted-fork contract in #1010 rather than silently consuming a newer unpromoted local/amicode revision.

Approach — Split rebuilds into two clear paths. Rebuild from Main pulls the latest main, downloads the pinned fork binary from its GitHub Release (no bun, no fork clone), builds the extension locally with pnpm, and deploys via atomic file-copy overlay. Rebuild Locally builds the fork binary from a local clone via bun for contributors editing the fork. Both paths auto-detect and provision missing dependencies (pnpm via corepack or npm fallback, bun via pinned curl | sh, fork clone via gh) with user consent; Node >= 20 and git are the hard prerequisites. Deployment uses backup-and-swap on the same filesystem instead of line-by-line file copy, with a pending-swap marker for crash recovery.

Approaches considered — Downloading the CI-built VSIX artifact was rejected because code --install-extension overwrites the Marketplace-installed extension and disrupts its update lifecycle. Always compiling the fork locally preserves current semantics but forces every user to install bun and clone the fork. Dynamic extension loading expands the trust surface beyond what is justified.

Scope — in: dependency detection and auto-provisioning (including gh CLI), promoted-fork binary download, local extension build from pulled main, atomic file-copy adoption with rollback and crash recovery, runtime path self-discovery, shell-wrapper parity, supported-host matrix, and legacy script retirement. out: native Windows execution (WSL only), fork-promotion automation (#1010), CI artifact download, code --install-extension adoption, and app-only hot swapping.

Assumptions#1010 establishes the reviewable promotion path from local/amicode into Amicode main. The opencode.lock.json on main always points to a verified GitHub Release with sha256-stamped platform binaries. CI already builds and verifies a VSIX on every main push (vsix-gate job), but that artifact is not consumed by rebuilds. The harmoniqs/opencode fork is currently public (HTTPS download works without auth); gh is a fallback for network issues and a requirement for fork-clone provisioning.

Acceptance Criteria

  • A macOS arm64, Linux x64, or Linux arm64 user with only Node >= 20 and git can complete Rebuild from Main without pre-installing bun, pnpm, or cloning the opencode fork. gh is required only if HTTPS download fails or for Rebuild Locally's fork-clone provisioning.
  • Rebuild from Main resolves its fork binary exclusively from opencode.lock.json on the pulled main branch. A newer local/amicode head is never downloaded, built, or selected.
  • Missing pnpm is auto-provisioned via corepack enable where corepack is available and unprivileged; falls back to npm exec -y pnpm@<pinned-version> when corepack is absent or requires elevated privileges. Missing bun (Rebuild Locally only) is auto-provisioned via a pinned, version-specific bun installer (curl -fsSL https://bun.sh/install | bash -s bun-v<pinned>), and verified after install (bun --version). Missing fork clone (Rebuild Locally only) is auto-provisioned via gh repo clone.
  • Every provisioning action is preceded by a consent prompt listing what will be installed and where. A headless bypass is available via AMICODE_AUTO_PROVISION=1 for CI and scripted flows.
  • Node absence and git absence are detected before any mutation and produce clear guidance naming platform-specific install methods (not a silent failure or opaque error).
  • Deployment backs up the current extension dist, stages new build output to a sibling directory on the same filesystem (never /tmp), swaps via rename(), and rolls back to the backup on any failure. A pending-swap marker file enables crash recovery on next activation.
  • No rebuild path copies individual files into the installed extension directory one at a time or edits VS Code settings.json directly.
  • A native Windows host performs zero rebuild mutation and directs the user to a WSL extension host. An Intel Mac (darwin-x64) detects the unsupported architecture before any download or build and shows the unsupported-host advice.
  • Both shell scripts (rebuild_amicode_locally.sh, rebuild_amicode_remotely.sh) delegate to the shared rebuild logic and preserve explicit source-root overrides.
  • The extension discovers its own binary and asset paths at runtime via context.extensionPath (which resolves correctly on WSL at ~/.vscode-server/extensions/ and on Remote-SSH). The fleet-guard amicode.opencodeBinary override remains a first-class, supported setting for fleet clients.
  • On macOS, Gatekeeper quarantine is cleared automatically on the downloaded binary via xattr -dr com.apple.quarantine <path> 2>/dev/null || true before codesigning. No manual Gatekeeper workaround is needed.

Testing Decisions

Add hermetic dependency-resolver tests with injected shell, filesystem, and network. Extend overlay-provenance fixtures from #1010 for manifest-pinned source selection. Add atomic-copy and rollback tests with injected filesystem, including crash-recovery via pending-swap marker. Add cold-host integration coverage for macOS arm64, Linux x64, Linux arm64, and Windows-to-WSL routing. Test corepack fallback on system-packaged Node. Test gh-absent fallback for HTTPS-only download.

Key Decisions

Two Rebuild Paths

Rebuild from Main is the common path: it needs no fork clone and no bun because the fork binary is downloaded from the GitHub Release pinned in opencode.lock.json. Rebuild Locally is the fork-contributor path: it builds the fork from a local clone and requires bun. Both share the same dependency resolver, build pipeline (for the extension), and deployment mechanism.

Hard Prerequisites: Node and Git

Node >= 20 and git cannot be safely auto-installed because the ecosystem has too many competing managers and auto-installing risks conflicting with the user's existing setup. gh is a soft dependency: needed for Rebuild Locally's fork-clone provisioning and as a download fallback, but not for the primary HTTPS download path on a public fork. Everything else (pnpm, bun, fork clone) installs to user-scoped directories with no system-level side effects.

corepack Fallback

corepack enable is the preferred pnpm provisioning path but is unavailable on distro-packaged Node (Ubuntu apt) and requires sudo on system-packaged installs. The resolver detects this and falls back to npm exec -y pnpm@<pinned>, which works in user space on any Node >= 20.

File-Copy Overlay, Not VSIX Install

Deployment copies the built dist into the already-installed extension directory rather than running code --install-extension. This preserves the Marketplace-installed extension's identity and auto-update lifecycle. The copy uses same-filesystem staging + rename instead of the current line-by-line approach, with a pending-swap marker for crash recovery.

Runtime Path Self-Discovery

The extension resolves its binary, app bundle, and asset paths from context.extensionPath at activation time. This works correctly on local installs (~/.vscode/extensions/), WSL/Remote-SSH (~/.vscode-server/extensions/), and VS Code Insiders (~/.vscode-insiders/extensions/). The fleet-guard opencodeBinary override remains first-class because it is a mode signal, not just a path preference.

Data Contracts

Contract Required content
Dependency check Tool name, required (bool), present (bool), version, sufficient (bool), provision method, target path, fallback method
Rebuild operation Mode (main/local), dependency check results, consent state, build phases, deployment outcome, rollback state
Deployment backup Backup path, timestamp, extension version, content hash
Pending-swap marker Backup path, target path, timestamp, swap state (pending/committed/rolled-back)

Error Handling

Every rebuild error is a classified catalog entry (src/rebuild_errors.ts), not raw stderr. Each error has a one-line message and numbered fix steps. Raw stderr is available as collapsed detail, never shown as the primary message.

Design rule: the user never sees a truncated shell error as the only information. The coordinator classifies the failure, picks the catalog entry, and renders: short error → numbered steps → expandable detail.

The catalog covers 30+ error cases across seven phases (pre-flight, git pull, lock-file validation, download, build, deployment, post-deployment). Examples:

Phase Error Message First fix step
Pre-flight Node missing "Node.js is not installed." "Install Node >= 20 from https://nodejs.org"
Pre-flight corepack fails "Could not set up pnpm automatically." "Try manually: npm install -g pnpm"
Download sha256 mismatch "The downloaded binary failed integrity verification." "This usually means a corrupted download. Retry the rebuild."
Deployment Swap failed "Could not deploy the new build into the extension directory." "Close all VS Code windows, then retry."
Post-deploy Health timeout "The extension did not start correctly after the rebuild." "The previous version has been restored automatically."

Constraints & Invariants

  • The promoted fork SHA from Amicode main's opencode.lock.json is the only Main rebuild source.
  • Auto-provisioning never modifies global PATH, system package managers, or VS Code settings.json.
  • Managed tools are invoked by absolute path (e.g., ~/.bun/bin/bun), never bare command name after provisioning.
  • Deployment preserves at least one backup of the prior extension dist. Staging happens on the same filesystem as the target.
  • A pending-swap marker enables recovery if the process crashes mid-swap.
  • Rollback is automatic on any post-swap failure; watchdog restores at most once.
  • Native Windows and Intel Mac never attempt a source rebuild.
  • The fleet-guard opencodeBinary setting is a first-class mode signal, not a deprecated override.
  • No rebuild error surfaces raw stderr as the primary message. All errors are classified catalog entries with fix steps.

Prior Art

  • pasqal_python.ts — auto-provision pattern (venv + pip, hash-gated stamps, existence check before trusting stamp).
  • julia_setup.ts — consent-gated install pattern (visible terminal, curl | sh).
  • fetch_opencode.mjs — release download with sha256 verification, HTTPS-first with gh fallback.
  • opencode_updater.ts — same-filesystem staging via mkdtempSync(path.join(root, ...)), consistentDbCopy() for SQLite WAL-safe backup.
  • chat_bridge.ts lines 555–850 — current rebuild handler (to be replaced).

Source

Durable record: #1017 records ADR 0015 and the repository glossary additions.
Blocked by #1010 for the promoted-fork source contract.

Sub-Issues

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions