Last updated

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 and the ctx API. This page picks up from a working extension.js.

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/<ext>/extension.js            (layout 2: one folder per extension, e.g. a shared team folder)
P/<ext>/<version>/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.

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

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

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, 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 <id>-<version>.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.

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

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/<id>/<version>/, 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).

The simplest path of all, for your own extensions, needs no packaging: drop the folder at <workspace>/.undra/extensions/<name>/extension.js and restart. That copy is also Unverified.

Publishing to the community 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.

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

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.

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.

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

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 for the format, the ctx surface, and the bundling setup.
  • The ctx API for exportApi / getExtensionApi and the rest of the runtime surface.
  • Commands & AI context for the other contribution types a packaged extension can ship.