Skip to content

Embedding

The showcase terminals use only the public exports of @aletheia-works/formicarium: createSession from /browser, and createCommandSession and createInstaller from the package root (the installer quietly installs the tools every page gets, such as nano). Your page can do the same.

This guide matches the formicarium 0.9.0 pack (the candidate's version). Install it from npm:

        npm install @aletheia-works/formicarium@0.9.0
      

The package on npm

  • Same origin. The Worker (runtime/web/package-worker.bundle.js) and the core under assets/paludarium/ (the loader index.mjs, the modules next to it, paludarium.wasm and build-info.json) must be served from your page’s origin. Pass the absolute URLs of the Worker, loader, wasm and build information as assets; the other modules are read next to the loader.
  • Cross-origin isolation. Serve COOP same-origin and COEP require-corp, or use a service worker such as coi-serviceworker on static hosting.
  • CSP. Allow same-origin scripts and Workers and WebAssembly compilation, for example script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'. blob: is not needed. The core is loaded from the asset URLs after its digests are checked, so serve them from an origin you trust.
  • Guests. Use a catalog (GuestCatalog) whose entries name same-origin guest files with their SHA256 and build-info.json. install <tool> checks every byte before it registers a guest.

This code is type-checked against the pack this site was built from.

embedding.ts
// The embedding guide shows this file as it is; the build type-checks it
// against the types of the candidate's pack.
import {
createCommandSession,
decodeUtf8,
type GuestCatalog,
} from '@aletheia-works/formicarium';
import { createSession } from '@aletheia-works/formicarium/browser';
/**
* Installs one tool from a catalog and prints its version. `packageBase` is
* the same-origin URL of the package files; the page must be cross-origin
* isolated (COOP/COEP headers or a service worker such as coi-serviceworker).
*/
export async function toolVersion(
packageBase: URL,
catalog: GuestCatalog,
tool: string,
): Promise<string> {
const session = await createSession({
cwd: '/work/app',
entries: [
{ path: '/work', type: 'dir', mode: 0o755 },
{ path: '/work/app', type: 'dir', mode: 0o755 },
],
assets: {
loaderURL: new URL('assets/paludarium/index.mjs', packageBase),
wasmURL: new URL('assets/paludarium/paludarium.wasm', packageBase),
workerURL: new URL('runtime/web/package-worker.bundle.js', packageBase),
buildInfoURL: new URL('assets/paludarium/build-info.json', packageBase),
},
});
try {
const shell = createCommandSession(session, { catalog });
await shell.execute(`install ${tool}`, {
onProgress: (progress) =>
console.log(progress.phase, progress.processedBytes),
});
const result = await shell.execute(`${tool} --version`);
return decodeUtf8(result.stdout);
} finally {
await session.dispose();
}
}

To open a terminal with tools already installed, install them with createInstaller before you create the command session. It runs the same checks as install <tool> but writes nothing, so the user never sees an install line. An installed tool runs by name in the command session, and every guest run finds it as /usr/bin/<tool>, so one tool can start another (for example a version control tool that opens an editor). To register guest bytes you already hold instead of a catalog entry, check them with validateGuest and pass the result to registerValidatedGuest.

preinstall.ts
// The embedding guide shows this file as it is; the build type-checks it
// against the types of the candidate's pack.
import {
type CommandSession,
createCommandSession,
createInstaller,
type GuestCatalog,
type Session,
} from '@aletheia-works/formicarium';
/**
* Installs `tools` before the user types anything, without writing to the
* terminal, then returns the command session for the terminal. Each tool runs
* by name there, and other guests find it as `/usr/bin/<tool>` (for example a
* version control tool that opens an editor).
*/
export async function terminalWithTools(
session: Session,
catalog: GuestCatalog,
tools: readonly string[],
): Promise<CommandSession> {
const installer = createInstaller(session, catalog);
for (const tool of tools) await installer.install(tool);
return createCommandSession(session, { catalog });
}

CommandSession.execute returns the original stdout and stderr bytes of each command. For a guest run they also arrive in chunks through onOutput; write one or the other, not both. Decode the bytes for display and never treat them as HTML. onProgress reports totalBytes: null when the total is unknown, which differs from 0. Pass an AbortSignal to cancel, and dispose the session when the page goes away.