@aistrike-dev/ui-shell-dev
v3.5.0
Published
A Vite plugin that serves the AiStrike UI shell from your micro front-end's dev server, plus a dev-token CLI.
Keywords
Readme
@aistrike-dev/ui-shell-dev
Serves the AiStrike UI shell from your micro front-end's (MFE's) own Vite dev server, and mints dev tokens.
Setup
Install it as a devDependency, next to @aistrike-dev/ui-shell-sdk, and add shellDevHost() after mfe():
import { mfe } from '@aistrike-dev/ui-shell-sdk/vite';
import { shellDevHost } from '@aistrike-dev/ui-shell-dev/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [mfe({ id: 'hello' }), shellDevHost()],
});vite now runs the shell as it is deployed: a console and a cell on two origins.
- The console is your Vite server: the shell at
/with a minted dev session. - The cell is a server on its own port: the console's plus 1000 (5103 → 6103), or any free port when that is past 65535
or taken.
shellDevHost({ cellPort })pins it; a pinned port that is taken is logged and the cell does not start. It forwards each domain's/<id>/ui/…and/<id>/api/…, WebSockets included, to the server that has it: your MFE (this Vite server) and the built-in MFEs, nothing else. - The dev token's
cellclaim names the cell on the address the browser used, so the shell loads every domain from it cross-origin, with CORS and the bearer token, exactly as fromhttps://<cell>/<id>when deployed. Your MFE's HMR runs through the cell too.
Your dev server serves the cell's layout: mfe() puts the MFE under the Vite base /<id>/ui/latest/ and answers
/<id>/api/… as /api/…, so your mock API keeps its /api/ paths.
To see your MFE beside other domains' dev servers, use the ui-shell repository's dev server
(pnpm --filter @aistrike-dev/ui-shell dev), whose cell forwards to every server its shell.dev.json lists.
Built-in MFEs
The package carries prebuilt copies of the MFEs that ship with the shell (assistant, settings and home) with their mock APIs, and the plugin serves each on its port (assistant 5104, settings 5101, home 5102), behind the cell. You get the whole frame (Home, Settings, the assistant pane on ⌘I / Ctrl+I) with nothing else running.
- A server already listening on one of those ports wins: the plugin leaves that port alone, so a real dev server for that MFE (with HMR) is used instead. A port freed later is taken over on the next page load.
- A real dev server for one of them started after this one fails, because the bundled copy holds its port. Start the one you are editing first, or restart this server after starting it.
shellDevHost({ platform: false })serves none;shellDevHost({ platform: ['assistant'] })only those listed.- The copies are as old as the package (in this repository, the last
pnpm nx build dev-host).
Keep Vite's server.allowedHosts at its default: the plugin names the cell in the dev token on the request's Host
address, which is only safe while Vite rejects unknown hosts. The cell applies the same rule itself: it answers only a
Host of localhost, *.localhost or an IP address, and 403 to anything else. The cell is plain HTTP, so serve the
console over HTTP too: the shell loads the cell over the console's own protocol.
shell.dev.json
Read from the Vite root when present (or from the plugin's devFile option).
{
"domains": [{ "id": "hello", "nav": { "label": "Hello" } }],
"idleTimeoutMs": 0,
"claims": { "permissions": ["hello:read"] }
}A domain's route prefix is always /<id>, so a domain has no route key, and the shell has no profiles, so the file
has no profile key: a leftover one of either stops the dev server with a message to delete it. The ids shell,
preferences, commands, api, assets, ui, config, signin and webhooks are reserved.
domains names your own domain when the shell does not compile it in (a devPort is accepted and ignored); naming
another domain, or an overrides or cell key, stops the dev server. idleTimeoutMs defaults to 0 (idle timeout off), and claims is merged
over the default dev claims (cell is the plugin's, not the file's). The plugin's claims, ttlMs and noToken options win over the file. "lab": true
shows Octopus UI's Design Lab (its live controls for the open design questions, in a Picture-in-Picture window) over
your pages, so you can judge them on your own screens. "lab": { "designSystem": "../octopus-ui" } also names an
Octopus checkout (relative to the Vite root): Lock in then writes a work-loop item into its .backlog/, through
Octopus's Lock-in plugin (@aistrike-dev/ui/lab/vite, loaded from the installed @aistrike-dev/ui). Without one, or
when the path is not an Octopus checkout, Lock in downloads the same item instead.
Legacy bridge (temporary)
legacyBridgeDev() signs you in through the old app and passes API calls to its backend, until real sign-in exists
(docs/superpowers/specs/2026-11-09-console-migration-design.md, section 6). Add it beside shellDevHost():
import { legacyBridgeDev, shellDevHost } from '@aistrike-dev/ui-shell-dev/vite';
export default defineConfig({
plugins: [react(), mfe({ id: 'alerts' }), shellDevHost(), legacyBridgeDev()],
});Set its variables in the environment or in .env.local (do not commit it). For preview:
LEGACY_BRIDGE_ORGS=preview
LEGACY_BRIDGE_DEFAULT_ORG=preview
LEGACY_BRIDGE_PRODUCT_DOMAIN=aistrike.com
LEGACY_BRIDGE_API_HOST=backend.preview.aistrike.com| Variable | Value |
| --- | --- |
| LEGACY_BRIDGE_ORGS | The org slugs to sign in to, comma-separated. next-auth for an org is at https://<org>.<product domain>. |
| LEGACY_BRIDGE_DEFAULT_ORG | The org a sign-in without ?org= uses. |
| LEGACY_BRIDGE_PRODUCT_DOMAIN | The domain the org hosts are under. |
| LEGACY_BRIDGE_API_HOST | The old app's API host. |
| LEGACY_BRIDGE_REFRESH_INTERVAL | Optional. Seconds a session is reported to live, so the shell refreshes on this interval. Default 600. |
With the plugin, the dev host starts signed out, the dev cell forwards /legacy-bridge/api/… to it, and the shell page
names that URL in the aistrike-legacy-bridge-base meta tag. The dev server does not start without the 4 required
variables. app-development needs the VPN; preview does not.
Commands
aistrike-shell token [--claims <path>] [--ttl 30d|12h|45m|90s] [--out <path>] [--json]token mints a dev token and prints it (or { token, session } with --json), optionally writing it to --out.
The package's own e2e (pnpm nx e2e dev-host) serves the settings MFE on port 5101, with the bundled home and assistant on 5102 and 5104; it fails loudly if a dev server already occupies 5101, and stop any on 5102 or 5104 first. It also checks that npm pack would publish the bundled copies.
Opening / in dev lands on your own MFE (the plugin injects landing with your domain id; shell.dev.json may set landing to another domain id; an id that is not in the shell's domain list falls back to /home). A domain that is neither bundled nor running shows as failed in the rail.
aistrike-shell serve --shell-dir <devhost build> [--config shell.dev.json] [--port 3000] [--cell-port 4000]serve is the ui-shell repository's dev server: the console and a cell whose shell.dev.json lists where each domain
runs, { "cell": { "domains": [{ "id": "lab", "nav": { "label": "Lab" }, "at": 5103 }] } } (at a port or an http: origin:
the cell forwards over HTTP; nav only for a domain the shell does not compile in). Its console and cell answer only
localhost, *.localhost and IP addresses. Every compiled domain not listed is the built-in MFE. A deployed
cell ("cell": "https://…") is refused until the shell can sign in to it.
Checking Fast Refresh (spike 1)
This is a manual check; no automated test covers it. From the repository root:
pnpm nx build dev-hostonce (builds the shell and copies it into the dev host package).pnpm --filter @aistrike-dev/app-settings dev.- Open
http://localhost:5101/settings/profile. - Edit
apps/settings/src/ProfilePage.tsx(for example, theProfileheading) and save. - Confirm the page updates without a full reload: text typed into a field beforehand is still there.
The dev token
The dev token is signed with a dev-only key stored under ~/.config/aistrike-shell/. It exists so the shell has a
session locally. Deployed environments never trust it.
