RFD 0032 — oxup manages editor-extension installation
- State: committed
- Opened: 2026-06-13
- Decides: how the Argon editor integration (the VS Code extension today; Neovim / Vim /
Emacs later) is installed and kept in sync with the active toolchain — by
oxup, abstracted over editors, rather than hand-installed. Closes the “users must hand-install the VS Code extension” gap. Builds on RFD 0013 (theoxupmanager + theargon.sharpe-dev.comCDN) and RFD 0013 (theoxupmanager + dist layout). - Implements: a new
oxup extension(aliasext) subcommand; anEditorIntegrationabstraction; a CDN asset path for the.vsix; auto-wiring fromoxup init/oxup update.
This RFD records, as built, the editor-extension story. The first cut shipped the VS Code
family (VS Code, Cursor, VSCodium, VS Code Insiders) via an --install-extension CLI; Neovim,
Vim, and Emacs (#393) now install by file placement — the embedded plugin tree plus a managed
config block in the user’s init file — since those editors have no install-CLI contract. Nothing is
silently skipped (the no-hollow-features house rule).
Question
Today the Argon VS Code extension (editors/vscode/, id argon-lang.argon) is built by
release.yml’s build-vsix job and attached to the GitHub Release. A user who wants it must find
the .vsix, download it, and run code --install-extension by hand — there is no version coupling
to the toolchain they installed, and nothing refreshes it when they oxup update. We already own
the install story for the toolchain (oxup install fetches a version-matched, sha256-verified
artifact from the CDN). The editor extension should ride the same rails.
- What is the command surface? One subcommand, abstracted over editors, so vim/neovim/emacs can slot in without a new top-level verb.
- How does the extension version stay coupled to the toolchain? A user on
stable0.2.1 must get the 0.2.1 extension, not “latest”. - Where does the asset live and how is it verified? Same discipline as the toolchain fetch: immutable versioned CDN path, sha256 sidecar, fail-closed.
- What happens for editors we don’t yet support? Loud refusal or silent skip?
Decision
1. Command surface — oxup extension (alias ext)
oxup extension install [--editor <vscode|cursor|codium|code-insiders|code-server|neovim|vim|emacs>] [--archive <path.vsix>] [--extensions-dir <dir>]
oxup extension uninstall [--editor <name>] [--extensions-dir <dir>]
oxup extension list
install(no--editor): auto-detect every installed VS Code-family editor (by its CLI on PATH) and install the extension matching the active toolchain version into each. With--editor, target exactly one. With--archive <path.vsix>, install a local.vsix(offline / a freshly built extension) instead of fetching from the CDN.uninstall: removeargon-lang.argonfrom the detected (or--editor-named) editors.list: show detected editors and, for each, the installedargon-lang.argonversion (or “not installed”).
2. Editor abstraction — EditorIntegration
An enum Editor with a small behavioral surface (oxup/src/extension.rs):
| method | meaning |
|---|---|
name() -> &str | the --editor key (vscode, cursor, codium, code-insiders, code-server, neovim, vim, emacs) |
detect() -> Option<PathBuf> | the editor’s CLI on PATH, or None |
install_argv(vsix) -> Result<Vec<OsString>> | the exact argv to install a .vsix |
uninstall_argv() -> Result<Vec<OsString>> | the exact argv to uninstall argon-lang.argon |
The VS Code family maps vscode→code, cursor→cursor, codium→codium,
code-insiders→code-insiders, code-server→code-server; detect by that CLI on PATH; install via
<cli> --install-extension <vsix> --force; uninstall via
<cli> --uninstall-extension argon-lang.argon.
code-server (browser-hosted, server-side VS Code — Daytona/ODE sandboxes) is a full member of
the family: it honors the same --install-extension/--uninstall-extension contract. A server
sandbox usually has only code-server on PATH (no desktop code), so it is in the auto-detect
order — but last, after the desktop editors, so a desktop editor wins when both are present.
Because the running code-server instance is launched with an explicit --extensions-dir, a fresh
code-server --install-extension would otherwise land in the default dir; the optional
--extensions-dir <DIR> argument on install/uninstall is appended to the editor argv to target
the dir the live server actually reads. The flag is accept-and-passthrough for the desktop CLIs too
(VS Code’s code supports it), optional, and omitted by default (the CLI’s default dir). code-server
has no macOS .app bundle, so its detection is PATH-only (no bundle fallback).
Neovim, Vim, Emacs have no --install-extension CLI, so they install by file placement
(oxup/src/editor_plugin.rs, #393): the plugin sources (editors/nvim/, editors/emacs/) are
embedded into the oxup binary, placed into the editor’s native package dir, and the user’s init
file gets a managed config block between begin/end sentinels that loads the plugin and points
ox lsp at the active toolchain.
- Neovim →
$XDG_CONFIG_HOME/nvim/pack/argon/start/argon(auto-loaded by Neovim’s built-in packages), managed block ininit.lua. - Vim →
~/.vim/pack/argon/start/argon(the syntax + filetype-detection floor; the Lua LSP client needs Neovim 0.8+), managed block in~/.vimrc. - Emacs →
~/.emacs.d/argononload-path, managed block in~/.emacs.d/init.el.
The block is idempotent and non-destructive: re-running rewrites only the region between the
sentinels (one block, never duplicated), leaving hand-written config untouched; uninstall strips
the block and removes the placed tree. The sentinels are commented in the init file’s own language
(-- for Lua, " for Vimscript, ;; for Lisp) so the line is never a syntax error. The
install_argv / uninstall_argv CLI helpers still refuse these editors loudly — they have no CLI
contract — and redirect to the file-placement path; nothing is a silent no-op.
Version coupling for file-placement editors. There is no CDN .vsix to address. Instead the
coupling is structural: oxup ships from the same release pipeline as the toolchain and carries the
same version, so the embedded plugin (including its generated version.lua / argon-version.el
stamp) matches the toolchain oxup installs, and oxup update (which self-updates oxup and
re-runs the install) refreshes it. The managed block additionally points ox lsp at the active
toolchain’s ox, so the editor always talks to the matching language server. Auto-wire stays
VS-Code-only: oxup init/update never write into a user’s init.lua/init.el unbidden;
file-placement editors install only on an explicit --editor.
The placement plan is constructed and asserted in tests over a temp tree without invoking a real
editor; only the write/create_dir_all/remove side effects run at the CLI boundary.
3. Asset source + version coupling
The extension version tracks the toolchain version. In release.yml’s build-vsix job, the
resolved $version is stamped into editors/vscode/package.json version before vsce package,
and the artifact is named argon-<version>.vsix. publish-dist uploads it (plus a bare-hex
.sha256 sidecar) to:
s3://argon-dist-sharpe/editors/vscode/<version>/argon-<version>.vsix
/editors/vscode/<version>/argon-<version>.vsix.sha256
CDN: https://argon.sharpe-dev.com/editors/vscode/<version>/argon-<version>.vsix
oxup extension install (no --archive) resolves the active toolchain’s concrete version (read
from the installed toolchain’s manifest.toml version, so a stable channel maps to the real
0.2.1), fetches editors/vscode/<version>/argon-<version>.vsix, sha256-verifies it against the
sidecar (same fail-closed discipline as the toolchain fetch), writes it to a temp file, and hands
that path to the editor CLI’s --install-extension. The .vsix is also still attached to the
GitHub Release (the secondary download path).
The CDN base URL is the existing dist_base_url() ($OXUP_DIST_URL, default
https://argon.sharpe-dev.com), so a mirror / smoke host is honored end to end.
4. Auto-wire from init / update
After the toolchain is placed:
oxup init(non-minimal) detects installed VS Code-family editors and installs/refreshes the matching extension.--no-extensionskips it;--minimalalready skips it (it doesn’t fetch a toolchain at all). If no editor is detected, print a quiet one-line note — not an error.oxup updaterefreshes the extension for the channel it updated, but only when the version changed: the installed extension version is recorded insettings.toml([extension] installed_version), andupdatere-installs only if the new toolchain version differs.
A failed extension install during init/update is a soft failure (warn, don’t abort): the
toolchain is what matters; the extension can be installed later with oxup extension install.
5. Editor-support matrix
| Editor | Status |
|---|---|
VS Code (code) | supported |
Cursor (cursor) | supported |
VSCodium (codium) | supported |
VS Code Insiders (code-insiders) | supported |
code-server (code-server) | supported — server-side VS Code (Daytona/ODE); --extensions-dir for the live server dir |
Neovim (neovim) | supported — file placement into nvim/pack/argon/start/argon + managed init.lua block (#393) |
Vim (vim) | supported — syntax + ftdetect floor into ~/.vim/pack/... + managed .vimrc block (#393) |
Emacs (emacs) | supported — .el package into ~/.emacs.d/argon + managed init.el block (#393) |
Why this shape
- One verb, editor-abstracted. A single
extensionsubcommand with anEditorenum keeps the vim/neovim/emacs work a matter of filling ininstall_argv, not adding CLI surface. The no---editorauto-detect mirrors how a user expects “install the extension” to just work across whatever VS Code-family editors they have. - Version coupling over “latest”. Stamping the toolchain version into the
.vsixand addressing it at an immutable/editors/vscode/<version>/path means a pinned toolchain gets a matching extension, and the CDN path is 1-year-cacheable like the toolchains. This avoids a “latest extension against an old toolchain” skew once the extension grows toolchain-coupled behavior (LSP protocol versions, server flags). - Reuse the fetch discipline. The
.vsixfetch reusesdist_base_url()and the same sha256-verify-before-use path as the toolchain, so there is no second, weaker download path. - Loud, not silent, for unsupported editors. Recognizing vim/neovim/emacs and refusing with a specific pointer (and an issue number) is the house rule: a no-op that pretends to work is worse than an honest “not yet.”
Out of scope / deferred
- Windows. v0.2 is macOS + Linux only; the VS Code CLIs exist on Windows but the rest of the
oxuplayout is unix-only (RFD 0013). - Marketplace / Open VSX publish. Argon is Sharpe-internal; the
.vsixis distributed via the private CDN + the GitHub Release, not a public marketplace.