Skip to main content
eject downloads a definition from the revision and writes its logic.code content back out as files on disk. It exists for one situation: a workflow that was authored in the visual editor and now needs to live in a repository. It is not the inverse of build. The code stored on the revision is the compiled code — the compiler rewrote every import into a flat-namespace rebinding (import numpy as np became np = numpy) and that transform has no inverse. What eject recovers is whatever structure the compiler’s source markers preserved, never the import statements they replaced. The command prints that caveat on every run that renders human output — it is part of the rendered form, so --json never carries it:
Treat the result as a starting point you finish by hand: restore the imports, split what wants splitting, then build and diff until the compiled output matches what was there.

Synopsis

There is no --dry-run. --force is the guard instead: without it, a run that would overwrite anything writes nothing at all.

What it does

  1. Resolves the blob and revision. This is a read, so a commit-phase revision is fine.
  2. Lists the revision’s definitions and matches them against the manifest.
  3. For each manifest entry that has a remote counterpart, downloads the definition and, for each declared component binding, reads its code port content and splits it into files.
  4. Checks every destination path for a conflict before writing any of them.
  5. Writes the files, prints one line per file, and prints a manifest snippet for each component that has no binding yet.
A manifest entry whose alias is not on the revision is skipped in silence — there is nothing to eject — and so is an entry that declares no components[] at all. eject only ever looks at bindings you have written down, so adopting a component means naming it in the manifest first, even when it has no code: block yet. Under --json the payload carries definitions — each with its alias, its destination root and the files written — plus bound, alongside the envelope’s schema_version. bound is true when the run printed no manifest snippets: nothing needs adding to the manifest.

Where the files land

The destination depends on whether the component is already bound: The rejection is deliberate. A bound component already has a root that build compiles from; accepting a second one would put the package in two places and leave build compiling the one you stopped editing. That split matches the two reasons to run this command. A bound component ejects into its own base_path — this is the recovery path out of REMOTE_EDIT and UNMANAGED_CODE, where someone edited the code in the browser and you need it on disk to reconcile by hand. An unbound component ejects into a fresh directory, because creating that binding is what adoption is, and the command prints the snippet to paste:
The suggested entry_point is the last file reconstructed, not the first. The compiler emits local modules in dependency-first order and preserves that order into content[], so the entry point — the module nothing else in the package imports — is always last. --output names one root for the whole run. Ejecting several unbound definitions into it at once is a mistake: two definitions whose markers share a relative path would land on the same file.

What the markers preserve

The compiler prefixes each module it emits with # blobhub:source <relative path>. eject reads those markers back:
  • Text under a marker becomes that marker’s file, at that relative path, nested directories included.
  • Text with no marker above it becomes one fallback file, named <alias>_<name>.py — where <name> is the binding’s name, or the literal code when the binding names its component by id. This is the browser case: code typed into the visual editor has no markers at all, so a definition that was never built by this CLI ejects as exactly one file.
  • A marker with an empty body still produces an empty file. An empty __init__.py compiles to its marker line and nothing else, and dropping it as “no content” would turn a package into a broken import.
  • Non-text parts of content[] are ignored.
Nothing is discarded. Text before the first marker is written to the fallback file rather than folded into the first marked one. A marker path is text inside a document this CLI did not necessarily write, so it is untrusted: an absolute path, an empty one, or one containing .. is refused with BASE_PATH_ESCAPE rather than allowed to escape the destination root. A binding that names a component with no code port contributes no files and no error. If a definition ejects nothing, check that the binding points at the component you meant.

Nothing is written until every path is clear

eject collects every destination first and checks all of them for an existing file. If any exists and --force was not passed, it raises EJECT_TARGET_EXISTS, naming every conflicting path, and nothing is written — not the files that would not have conflicted, not a partial directory tree. The same holds for the --output-on-a-bound-component refusal. A refused run leaves the filesystem exactly as it was. With --force, existing files are overwritten. There is no backup; a checkout with uncommitted changes is the only thing standing between --force and losing them.

Errors

Every code above, with its remediation, is in Error codes.

See also