# Packaging & distribution This page covers everything after you can build an extension: iterating with live reload, bundling and signing it, sideloading, publishing, and how one extension can depend on another. If you have not built an extension yet, start with the [Overview](/docs/extensions/) and the [ctx API](/docs/extensions/ctx-api/). This page picks up from a working `extension.js`. ## Dev mode {#dev-mode} While you iterate, you do not want to repackage and reinstall on every save. Point Undra at a folder on disk and it scans, loads, and watches your extension live. Open **Settings → Extensions** and add an extension folder (a **search path**). Undra scans it, lists every extension it finds, and reloads each one on save. Found does not mean running: newly discovered extensions start **disabled**, and you enable the ones you want. The one exception, to keep the solo flow frictionless, is that adding a search path which contains exactly one extension enables it immediately. ### Accepted folder layouts A search path `P` is scanned to a depth of 2, and Undra accepts three layouts. The entry file is always named `extension.js`. ``` P/extension.js (layout 1: P itself is the extension) P//extension.js (layout 2: one folder per extension, e.g. a shared team folder) P///extension.js (layout 3: versioned folders, e.g. P/book/1.2.3/extension.js) ``` In layout 3, when a child folder contains only version-shaped subfolders (`1.2.3`, `0.4`, `2.0.0-beta.1`, optionally `v`-prefixed), Undra picks the **highest** version that actually has an `extension.js`, and shows that version in the hub. Child folders without an `extension.js` at an accepted depth are skipped silently, so a mixed folder (fonts, templates, whatever) just works because only extension-shaped folders surface. :::note Discovery never runs your code Scanning only LISTS folders. Nothing imports or evaluates your `extension.js` during discovery. Code runs only for the extensions you explicitly enabled. If a sibling `manifest.toml` or `manifest.json` is present, Undra cheaply reads it for a nicer display name (regex or `JSON.parse`, never execution) and falls back to the folder name. ::: ### Live reload Once an extension is enabled, Undra watches its entry file and reloads on change roughly every 1.5 seconds. It compares the entry file's modified-time and size each tick; if that listing is unavailable it falls back to hashing the file's contents (these files are a few KB, so even the fallback is cheap). Error recovery is the core of the loop. A broken save flips the extension's row to an error state (with one toast per transition into error, not on every poll), but the watcher keeps polling. The next save that fixes the syntax error reactivates the extension automatically, with no manual step. ```bash # Typical iterate loop: run your bundler in watch mode so it rewrites # extension.js on every save. With esbuild, use context() + ctx.watch() # rather than the one-shot build() from the overview. Undra's watcher then # picks up the new extension.js ~1.5s later and reloads automatically. node build.mjs ``` :::tip Dev copy wins over a catalog copy The main reason to use a search path is iterating on an extension that is also installed from the catalog. If your enabled dev module's `manifest.id` is already active, Undra disposes the live copy first and activates your dev copy in its place. Dev wins while enabled. Disabling the dev copy does not resurrect the installed one in the same session; a restart restores it. ::: If a previously enabled extension's folder disappears from a scan, its activation is disposed and the row goes to a missing state, but the enabled flag is kept, so it reactivates on a later scan if the folder reappears (for example, a shared folder that briefly unsyncs). Re-scan happens on boot, when you add a search path, and via the hub's manual **Rescan** action. ## Packaging {#packaging} For anything beyond a single hand-written file (multiple files, TypeScript, npm libraries), write it like normal software and bundle it into one `extension.js` with a build step. The loader needs exactly one ES module with no runtime imports, and a bundler produces precisely that. The full bundling setup (project layout, sharing Undra's React via `ctx.runtime`, the esbuild config) lives in the [Overview's "Scaling up" section](/docs/extensions/#scaling), so it is not repeated here. ### The `.undx` package To distribute, package your bundled folder into a `.undx`, the portable form of an extension. A `.undx` is a ZIP carrying: - the entry script (`extension.js`), - a machine-readable `manifest.json` snapshot, embedded so the installer can place and activate the extension **without ever evaluating its JavaScript**, - any sibling assets (hero image, screenshots). In dev mode, the per-extension **Package** action reads the module's live exported `manifest`, snapshots it to JSON, and hands the folder to the desktop app, which does the actual zipping and shows a native save dialog. The default filename is `-.undx`. ### Signing and the Verified badge A `.undx` published through Undra's community catalog is **signed with an Ed25519 key**. The signature lives in a `signature.json` entry inside the ZIP and is computed over a canonical digest of every other entry (entries sorted by name, each length-prefixed, hashed with SHA-256). A package earns the **Verified** badge only when that signature verifies against Undra's bundled catalog public key. :::warn Trust is signature-derived, never a flag "Verified" comes from a valid catalog signature over the package contents, computed at install time. It is never read from a field in the manifest. A crafted package could set any flag it likes, so Undra ignores manifest claims of trust entirely. The private signing key lives only in Undra's catalog repo, so you cannot self-sign a Verified package. ::: ## Sideloading {#sideloading} You can install a package directly, bypassing the catalog, with **Install from File**. Pick a `.undx` and Undra validates it, extracts it (zip-slip safe) into `~/.undra/community/extensions///`, records the install, then blob-imports and activates the entry script. The end state is indistinguishable from a catalog install, so update, disable, and uninstall all work on a sideloaded extension with no special-casing. A sideloaded package shows as **Unverified** unless it happens to carry a valid Undra catalog signature, which almost none will. Unverified is fine for your own work; treat anyone else's with care (see [Trust](#trust)). The simplest path of all, for your own extensions, needs no packaging: drop the folder at `/.undra/extensions//extension.js` and restart. That copy is also Unverified. ## Publishing to the community catalog {#catalog} The intended publishing destination is the **community catalog**: you submit a signed `.undx`, and users install it from inside Undra with one click, where it shows the Verified badge. :::warn The catalog is not live yet The signing, verification, and install-from-catalog machinery exists in the app, but the community catalog itself is not yet a shipping destination you can publish to. Until it is, the available-today paths are: drop a folder in `.undra/extensions/`, use a dev-mode search path, or share a `.undx` for someone to **Install from File** (shown as Unverified). The catalog and the Verified flow are the forthcoming path, not a live one. ::: ## Dependencies between extensions {#dependencies} One extension can publish an API and another can consume it. The producer calls `ctx.exportApi(api)`; the consumer calls `ctx.getExtensionApi(id)` and declares the relationship in its manifest so the host can order activation correctly. ```javascript file="extension.js" export const manifest = { id: 'community.example.charts-pro', version: '1.0.0', displayName: 'Charts Pro', publisher: 'example', capabilities: ['canvasWidgets.registry'], // Bare id = required, any version. Object form adds a semver range // and/or marks the dep optional. dependencies: [ 'community.example.ui-kit', // required, any version { id: 'community.example.theme', version: '^2.0.0' }, // required, semver range { id: 'community.example.icons', optional: true }, // never gates activation ], } export function activate(ctx) { const uiKit = ctx.getExtensionApi('community.example.ui-kit') // ALWAYS null-check: a dependency may activate and export nothing. if (uiKit && typeof uiKit.makeButton === 'function') { // ... use uiKit } // Publish your own API for dependents: ctx.exportApi({ version: 1, makeChart(opts) { /* ... */ } }) } ``` ### How declarations resolve `manifest.dependencies` accepts two forms: - a **bare id string**, shorthand for a required dependency with no version constraint, and - an **object** `{ id, version?, optional? }`, where `version` is a semver range (an omitted range or `'*'` means any version) and `optional: true` marks a dependency that never gates activation. The host normalizes these (dropping self-edges and duplicate ids, first declaration wins), resolves them into a graph, and activates dependencies **before** dependents (topological order). A required dependency that is missing, disabled, or version-incompatible gates the dependent off (with a reason surfaced in the hub's Dependencies tab). Optional dependencies never gate and never affect ordering, so an all-optional cycle is fine; a required-edge cycle skips its members. :::warn Always null-check `getExtensionApi` Topological order guarantees a required dependency has finished activating before your `activate()` runs. It does NOT guarantee the dependency exported anything: "dependency satisfied" gates activation, not API presence. A dependency may activate and call no `exportApi` at all, so `getExtensionApi(id)` can return `undefined`. For an optional dependency it returns `undefined` whenever the dependency is absent. Always null-check before use. ::: ## Trust {#trust} A loadable extension runs with **full access**: it can read and write files and reach the network. There is no sandbox. Only run extensions you trust. Practically: - Your own extensions (folder-drop, dev-mode, your own `.undx`) are Unverified and fine to run, because you wrote them. - A `.undx` from someone else is Unverified too; read its `extension.js` before enabling it, the same way you would review any code you are about to run with full machine access. - The Verified badge, once the catalog ships, means the package carries a valid Undra catalog signature over its exact contents. It proves provenance and integrity (this is the package Undra signed, unmodified), not that the code is safe. ## Where to go next - [Overview](/docs/extensions/) for the format, the `ctx` surface, and the bundling setup. - [The ctx API](/docs/extensions/ctx-api/) for `exportApi` / `getExtensionApi` and the rest of the runtime surface. - [Commands & AI context](/docs/extensions/commands-and-context/) for the other contribution types a packaged extension can ship.