API reference

platform

Every export of @kitr/platform, @kitr/platform/react, @kitr/platform/provider, @kitr/platform/devtools, @kitr/platform/shell, @kitr/platform/cli, read from the published type definitions. The guide shows them in use.

@kitr/platform

createPlatform

function createPlatform(app: KitrApp, driver?: PlatformDriver): Platform;

Build the platform service from a driver (default: the registered one, else the browser/server default). The provider binds it for you.

deepLinkPath

function deepLinkPath(url: string, prefixes?: string[]): string;

Strip a configured prefix from a deep link so it can be handed to the router.

defaultDriver

function defaultDriver(): PlatformDriver;

The browser (or server) driver used when no platform package registered one.

detectRuntime

function detectRuntime(scope?: RuntimeScope): Runtime;

Detect the runtime: Tauri by window.__TAURI_INTERNALS__, Capacitor by Capacitor.isNativePlatform(), Electron by the preload bridge, process.versions.electron or an Electron/ user agent. Everything else, including the server, is web. Safe to call anywhere: it only reads globals.

driverUnavailable

function driverUnavailable(runtime: Exclude<Runtime, 'web'>, pkg: string, detail?: string): KitrError;

The error a device package throws when its driver for a native shell is not implemented (or its plugin is not installed): it names the runtime and the package so the fix is obvious. Never a silent fallback to the web driver; apps that want the WebView behaviour pass fallbackToWeb.

fallbackFormFactor

function fallbackFormFactor(platform: Pick<Platform, 'type'>, options?: FormFactorOptions): FormFactor;

The form factor assumed before the window can be measured: a desktop shell is wide with a pointer, everything else is a phone.

forPlatform

function forPlatform<T>(table: Partial<Record<PlatformKey, T>> & {
  default: T;
}): T;

Resolve a per-platform table at build time, from import.meta.env.KITR_PLATFORM and KITR_PLATFORM_TYPE, so the choice costs nothing at run time and needs no app instance:

export const Route = createFileRoute('/feed')({ ...forPlatform({ www: { ssr: true }, default: { ssr: false } }) })

The platform name wins over its type, then default. Outside a kitr build the table's default is returned.

isDriverUnavailable

function isDriverUnavailable(error: unknown): error is KitrError;

Whether error is a driverUnavailable() error.

loadShellModule

function loadShellModule<T = unknown>(specifier: string): Promise<T>;

Load an optional module by specifier: through the loader a shell registered, else a plain dynamic import() (which works under Node and in tests, and fails in a bundled page where nothing registered it). Rejects when the module cannot be loaded; callers turn that into driverUnavailable() with the install instructions.

osFromUserAgent

function osFromUserAgent(ua: string): PlatformOs;

Map a user agent string to a PlatformOs; iPhone and iPad user agents are checked before macOS.

perPlatform

function perPlatform<T>(platform: Pick<Platform, 'type' | 'os' | 'native'> & {
  name?: PlatformName | undefined;
}, impls: Partial<Record<PlatformKey, () => T>> & {
  default: () => T;
}): T;

Pick an implementation per platform without branching in components:

const save = perPlatform(platform, { web: () => download, mobile: () => share, desktop: () => saveDialog })

Keys are platform names from kitrrc.ts, platform types or OS names; native covers every native shell; default is required. The most specific wins: name, then OS, then type, then native.

platformName

function platformName(): PlatformName | undefined;

The platform name this bundle was built for, as a build-time constant: kitr dev phone defines import.meta.env.KITR_PLATFORM as 'phone', and kitr types types it as the union of the names in kitrrc.ts. Compare it directly (if (platformName() === 'phone')) and the bundler drops the other branch, which is how a page or a feature stays out of a platform's bundle entirely. Undefined outside a kitr build (tests, plain Node).

platformType

function platformType(): PlatformTypeName | undefined;

The type (web, mobile, desktop...) of the platform this bundle was built for, from import.meta.env.KITR_PLATFORM_TYPE.

readFormFactor

function readFormFactor(win: Pick<Window, 'innerWidth' | 'innerHeight' | 'matchMedia'>, options?: FormFactorOptions): FormFactor;

The form factor a browser window reports right now. Not for the server: use fallbackFormFactor() there.

registerShellModules

function registerShellModules(loaders: Record<string, ModuleLoader>): void;

Register loaders for the optional packages a native shell's drivers import lazily (the Tauri plugins, say). A shell's provider calls this with literal import('...') expressions, so the app's bundler sees and bundles them; the device packages then ask for them by name with loadShellModule() and never name an optional package in code that every app bundles.

runtimeOf

function runtimeOf(app?: Pick<KitrApp, 'container'>): Runtime;

The runtime an app is in: the platform service's when it is registered (the platform packages set it from their shell), else detectRuntime(). Device packages call this to pick a driver.

selectRuntimeDriver

function selectRuntimeDriver<D>(runtime: Runtime, drivers: RuntimeDrivers<D>, options?: SelectDriverOptions): D;

Pick the driver for runtime from a table of factories. web calls its factory; a native runtime calls its own, and when that throws E_DRIVER_UNAVAILABLE the error propagates unless fallbackToWeb is set. Any other error propagates untouched.

widthClass

function widthClass(width: number, breakpoints?: FormFactorOptions['breakpoints']): WidthClass;

Classify a width in CSS pixels.

defaultBreakpoints

const defaultBreakpoints: {
  readonly medium: 768;
  readonly expanded: 1024;
};

Where medium and expanded start, in CSS pixels, when formFactor.breakpoints is not configured.

defineConfig

const defineConfig: <const C extends PlatformConfig>(config: C) => C;

Typed helper for config/platform.ts.

DRIVER_UNAVAILABLE

const DRIVER_UNAVAILABLE = "E_DRIVER_UNAVAILABLE";

The error code driverUnavailable() carries, for error.code === DRIVER_UNAVAILABLE checks.

runtimes

const runtimes: readonly Runtime[];

Every runtime, in the order detectRuntime() checks the native ones after web.

FormFactor

interface FormFactor {
  width: WidthClass;
  input: InputKind;
  orientation: Orientation;
  /** False on the server and during the first client render, when the values are the configured fallback. */
  ready: boolean;
}

What useFormFactor() answers: the three axes a responsive multi-platform app branches on, kept apart on purpose. Layout follows width, affordances (hover, hit sizes) follow input, idioms follow the platform itself (usePlatform()).

FormFactorOptions

interface FormFactorOptions {
  /** Minimum widths in CSS pixels. Default `{ medium: 768, expanded: 1024 }`. */
  breakpoints?: {
    medium?: number;
    expanded?: number;
  };
  /**
   * What the server and the first client render assume. Default: `expanded` + `pointer` in a desktop
   * shell, `compact` + `touch` everywhere else (which is what a phone, the most common SSR visitor, is).
   */
  fallback?: Partial<Pick<FormFactor, 'width' | 'input' | 'orientation'>>;
}

Options of useFormFactor() and of formFactor in config/platform.ts.

Platform

interface Platform {
  /** The active driver's platform type. */
  readonly type: PlatformType;
  /** True inside a native shell (Capacitor, Tauri, Electron). */
  readonly native: boolean;
  /**
   * The host runtime: `web`, `electron`, `tauri` or `capacitor`. Device packages
   * (`native`, `files`, `notifications`, `secure-storage`, `updater`) pick their driver by it.
   */
  readonly runtime: Runtime;
  /** The operating system, read from the driver on every access. */
  readonly os: PlatformOs | string;
  /** Installed as an app: a standalone PWA or any native shell. */
  readonly standalone: boolean;
  /**
   * The platform name from kitrrc.ts the bundle was built for, when known. After `kitr types`
   * it is the union of the names in `kitrrc.ts` (`'site' | 'phone'`), so `platform.name === 'tablet'`
   * is a compile error in a project with no such platform.
   */
  readonly name: PlatformName | undefined;
  /** The active driver, for anything the service does not wrap. */
  readonly driver: PlatformDriver;
  /** Whether the device reports a connection. */
  isOnline(): boolean;
  /** The native app version, or `'0.0.0'` where there is none. */
  appVersion(): Promise<string>;
  /** Foreground (`true`) and background (`false`) changes. Returns an unsubscribe function. */
  onAppState(listener: (active: boolean) => void): () => void;
  /** Hardware or window back button. Never fires where the driver has none. Returns an unsubscribe function. */
  onBackButton(listener: (info: {
    canGoBack: boolean;
  }) => void): () => void;
  /** URLs the OS opens in the running app. Returns an unsubscribe function. */
  onDeepLink(listener: (url: string) => void): () => void;
  /** The router path for a deep link: `deepLinkPath(url, deepLinkPrefixes)` with the prefixes from `config/platform.ts`. */
  linkPath(url: string): string;
  /** Connection changes. Returns an unsubscribe function. */
  onOnline(listener: (online: boolean) => void): () => void;
  /** Safe-area insets in pixels. */
  safeArea(): SafeArea;
  /** Open a URL in the system browser (native) or a new tab (web). */
  openExternal(url: string): Promise<void>;
  /** Close the app where the driver supports it; resolves and does nothing elsewhere. */
  exit(): Promise<void>;
  /** `platform.is('mobile')`, `platform.is('ios')`, `platform.is('phone')`: matches the type, the OS or the platform name from kitrrc.ts. */
  is(what: PlatformType | PlatformOs | PlatformName): boolean;
}

The platform service: the active driver plus safe defaults for everything it leaves out.

PlatformConfig

interface PlatformConfig {
  /** Path prefixes (`'/open'`) to strip from deep links in `platform.linkPath()` and `useDeepLink()`. The scheme and host are always dropped. */
  deepLinkPrefixes?: string[];
  /** Width classes and the first-render guess for `useFormFactor()`. */
  formFactor?: FormFactorOptions;
}

The options in config/platform.ts.

PlatformDriver

interface PlatformDriver {
  /** `web`, `mobile`, `desktop`, `server` or a custom platform type. */
  type: PlatformType;
  /** True inside a native shell (Capacitor, Tauri, Electron). */
  native: boolean;
  /** The host runtime: `web`, `electron`, `tauri` or `capacitor`. Default: `web` for a browser driver, the detected runtime for a native one. */
  runtime?: Runtime;
  /** The operating system, read on every access. */
  os(): PlatformOs | string;
  /** Installed as an app (PWA standalone, native shell). */
  standalone?(): boolean;
  /** The native app version. Default `'0.0.0'`. */
  appVersion?(): Promise<string>;
  /** Foreground/background. */
  onAppState?(listener: (active: boolean) => void): () => void;
  /** Hardware or window back button. */
  onBackButton?(listener: (info: {
    canGoBack: boolean;
  }) => void): () => void;
  /** Deep links and custom-scheme URLs opened while the app runs. */
  onDeepLink?(listener: (url: string) => void): () => void;
  /** Connection changes. Default: the browser's `online` and `offline` events. */
  onOnline?(listener: (online: boolean) => void): () => void;
  /** Whether the device has a connection. Default: `navigator.onLine`. */
  isOnline?(): boolean;
  /** Safe-area insets. Default: the CSS `env(safe-area-inset-*)` values. */
  safeArea?(): SafeArea;
  /** Open a URL in the system browser (native) or a new tab (web). */
  openExternal?(url: string): Promise<void>;
  /** Exit the app where the platform allows it (Android back at root, desktop). */
  exit?(): Promise<void>;
}

What a platform package implements so @kitr/platform can answer for it. Register one with app.drivers.register('platform', driver); every optional member falls back to a browser default.

RuntimeScope

interface RuntimeScope {
  /** Tauri puts its internals on `window`. */
  __TAURI_INTERNALS__?: unknown;
  /** The bridge the Electron preload script exposes (`exposeIpc()`). */
  kitrIpc?: unknown;
  /** The Capacitor runtime global. */
  Capacitor?: {
    isNativePlatform?(): boolean;
  };
  /** Node's `process`, present in Electron's main process and in Node-integrated renderers. */
  process?: {
    versions?: {
      electron?: string;
    };
  };
  navigator?: {
    userAgent?: string;
  };
  /** When the scope is a worker or test global that holds a separate `window`, the checks look there first. */
  window?: RuntimeScope;
}

The globals detectRuntime() reads. Pass a fake in tests; the default is globalThis.

SafeArea

interface SafeArea {
  /** Inset from the top edge (status bar, notch). */
  top: number;
  /** Inset from the right edge (notch in landscape). */
  right: number;
  /** Inset from the bottom edge (home indicator). */
  bottom: number;
  /** Inset from the left edge (notch in landscape). */
  left: number;
}

Safe-area insets in CSS pixels: the space the notch, status bar and home indicator cover.

SelectDriverOptions

interface SelectDriverOptions {
  /**
   * Use the web driver when the native shell has no driver of its own. Off by default: a native
   * shell without a driver throws `E_DRIVER_UNAVAILABLE` instead of silently running web code in a WebView.
   */
  fallbackToWeb?: boolean;
}

Options every device package's selectDriver() accepts.

InputKind

type InputKind = 'touch' | 'pointer';

The primary input: a finger or a mouse/trackpad.

ModuleLoader

type ModuleLoader = () => Promise<unknown>;

Loads one optional module, such as () => import('@tauri-apps/plugin-dialog').

Orientation

type Orientation = 'portrait' | 'landscape';

Which way the viewport is taller or wider.

PlatformKey

type PlatformKey = PlatformName | PlatformType | PlatformOs | 'native';

Keys perPlatform() and forPlatform() accept: a platform name from kitrrc.ts, a type, an OS, native, or default.

PlatformOs

type PlatformOs = 'ios' | 'android' | 'macos' | 'windows' | 'linux' | 'server' | 'unknown';

The operating system a driver reports; server during server rendering, unknown when the user agent matches none.

PlatformType

/** The kind of place the app runs: `web`, `mobile`, `desktop`, `server`, or a custom platform's own type. */
type PlatformType = 'web' | 'mobile' | 'desktop' | 'server' | (string & {});

The kind of place the app runs: web, mobile, desktop, server, or a custom platform's own type.

Runtime

type Runtime = 'web' | 'electron' | 'tauri' | 'capacitor';

Which host the code is running in, as one value: a browser (or the server) is web, and the three native shells are electron, tauri and capacitor. It answers "which driver should I load?", while platform.type answers "what kind of place is this?" (desktop covers both Electron and Tauri).

RuntimeDrivers

type RuntimeDrivers<D> = Record<Runtime, () => D>;

One driver factory per runtime. web always works; the others may throw driverUnavailable().

WidthClass

type WidthClass = 'compact' | 'medium' | 'expanded';

The width class of the viewport: a phone, a small tablet or a large window.

@kitr/platform/react

Except

function Except({ children, fallback, ...match }: OnlyProps): import("react").JSX.Element;

The inverse of <Only>: render children everywhere the match fails.

Only

function Only({ children, fallback, ...match }: OnlyProps): import("react").JSX.Element;

Render children only on the platforms, OS, shells or form factors given:

<Only platform="phone"><TabBar /></Only>
<Only width="expanded" input="pointer" fallback={<MobileNav />}><SideRail /></Only>

platform is typed against the names in kitrrc.ts once kitr types has run. Width, input and orientation come from useFormFactor(), so they are hydration-safe: the first render uses the configured fallback. This hides at run time; to keep code out of a platform's bundle, branch on platformName() or import.meta.env.KITR_PLATFORM instead.

useAppState

function useAppState(): boolean;

Foreground (true) or background. Backed by Capacitor App on mobile, window focus on desktop, visibility on the web.

useBackButton

function useBackButton(handler: (info: {
  canGoBack: boolean;
}) => void): void;

Android hardware back button, desktop window close. Return nothing to let the default run.

function useDeepLink(handler: (url: string, path: string) => void): void;

Deep links opened while the app runs. The handler gets the URL and the router path for it, with the scheme, host and deepLinkPrefixes from config/platform.ts removed:

useDeepLink(useCallback((_url, path) => void navigate({ to: path }), [navigate]))

useFormFactor

function useFormFactor(options?: FormFactorOptions): FormFactor;

The viewport's form factor: width class (compact under 768px, medium, expanded from 1024px), primary input (touch or pointer) and orientation. Hydration-safe: the server and the first client render return the configured fallback with ready: false, then the real values arrive after mount and follow window resizes and orientation changes.

Layout decisions belong to width, affordances to input, platform idioms to usePlatform():

const { width } = useFormFactor()
return width === 'expanded' ? <SplitView /> : <Stack />

useOnline

function useOnline(): boolean;

true while the device reports a connection. Hydration-safe: the server and first render say online.

usePlatform

function usePlatform(): Platform;

The platform service from the container.

usePlatformMatch

function usePlatformMatch(match: PlatformMatch): boolean;

Whether the current platform and form factor satisfy a PlatformMatch. The hook behind <Only> and <Except>.

useRuntime

function useRuntime(): Runtime;

The host runtime: web, electron, tauri or capacitor. Use it to branch on the shell itself; usePlatform().type says desktop for both Electron and Tauri. Without the platform provider it falls back to detectRuntime().

useSafeArea

function useSafeArea(): SafeArea;

Safe-area insets (notch, home indicator). Zero on the server and first render.

OnlyProps

interface OnlyProps extends PlatformMatch {
  children?: ReactNode;
  /** Rendered when the match fails. Default: nothing. */
  fallback?: ReactNode;
}

Props of <Only> and <Except>: the platform match plus children and fallback.

PlatformMatch

interface PlatformMatch {
  /** A platform name from kitrrc.ts: `'phone'`, or `['phone', 'tablet']`. Typed after `kitr types`. */
  platform?: OneOrMany<PlatformName>;
  /** The platform type the active driver reports: `'mobile'`, `'desktop'`, `'web'`, `'server'`. */
  type?: OneOrMany<PlatformType>;
  /** The operating system: `'ios'`, `'android'`, `'macos'`... */
  os?: OneOrMany<PlatformOs | (string & {})>;
  /** `true` inside a native shell (Capacitor, Electron, Tauri), `false` in a plain browser. */
  native?: boolean;
  /** The viewport width class from `useFormFactor()`. */
  width?: OneOrMany<WidthClass>;
  /** The primary input from `useFormFactor()`. */
  input?: OneOrMany<InputKind>;
  /** The orientation from `useFormFactor()`. */
  orientation?: OneOrMany<Orientation>;
}

What <Only> and <Except> match on. Every prop is optional and all given props must match (a logical AND). An array matches any of its values.

@kitr/platform/provider

platformProvider

const platformProvider: import("@kitr/core").KitrProvider;

Binds the platform service from the active platform driver. Platform packages (@kitr/mobile/provider, @kitr/desktop/provider...) register their driver in register(), so list them before this provider in kitrrc.ts; kitr add keeps that order.

@kitr/platform/devtools

panels

function panels(app: KitrApp, ui: DevtoolsUi): DevtoolsPanel[];

The Platform panel: active platform, OS and which package supplies each capability driver.

@kitr/platform/shell

createShell

function createShell(options: ShellOptions): Shell;

Build the Shell helpers for a configuration.

createShellDriver

function createShellDriver(options: ShellOptions, shell?: Shell): PlatformDriver;

The platform driver a shell registers: native, standalone, app state from visibility, plus the shell's own hooks.

createShellProvider

function createShellProvider(options: ShellOptions): KitrProvider;

The runtime provider of a native shell. Inside the shell it registers the platform driver and whatever register() adds; in a plain browser it does nothing, so the web build of the same app keeps working. Logs the runtime to devtools at boot.

export const desktopProvider = createShellProvider({
  name: 'desktop', platform: 'desktop', type: 'desktop', runtime: 'electron',
  driver: (shell) => ({ exit: () => shell.invoke('kitr:app:quit').then(() => {}) }),
})

userAgentOs

function userAgentOs(): PlatformOs;

The operating system from the user agent, unknown on the server.

visibilityAppState

function visibilityAppState(listener: (active: boolean) => void): () => void;

The foreground/background default every shell shares: the page's visibility.

Shell

interface Shell {
  /** The native runtime this provider wraps. */
  readonly runtime: ShellRuntime;
  /** The bridge, or undefined when the shell has none or the page is not inside it. */
  bridge(): ShellBridge | undefined;
  /** Call the bridge. Rejects with a message naming the provider when the bridge is missing. */
  invoke<T = unknown>(channel: string, ...args: unknown[]): Promise<T>;
  /** The operating system the shell reports. */
  os(): PlatformOs | string;
  /** True when the page is running inside this shell. */
  active(): boolean;
}

What createShellProvider() hands to its hooks: the pieces every shell uses.

ShellBridge

interface ShellBridge {
  /** Call the native side and resolve with its answer. */
  invoke(channel: string, ...args: unknown[]): Promise<unknown>;
  /** Listen to a channel the native side sends on. Returns the unsubscribe function. */
  on?(channel: string, listener: (...args: unknown[]) => void): () => void;
  /** Send a one-way message, with no reply. */
  send?(channel: string, ...args: unknown[]): void;
}

The untyped message bridge a shell exposes to the page: Electron's preload kitrIpc, a Tauri command channel.

ShellOptions

interface ShellOptions {
  /** The provider name, shown by `kitr doctor` and in errors: `desktop`, `tauri`, `mobile`. */
  name: string;
  /** The tag drivers are registered under (`app.drivers.describe()`, devtools): `desktop`, `mobile`. */
  platform: string;
  /** The `platform.type` the shell reports. */
  type: PlatformType;
  /** Which native runtime this is. */
  runtime: ShellRuntime;
  /** Whether the page is inside this shell. Default: `detectRuntime() === runtime`. */
  detect?(): boolean;
  /** Where the page finds the IPC bridge. Omit for shells that call native plugins directly. */
  bridge?(): ShellBridge | undefined;
  /** The operating system. Default: from the user agent. */
  os?(): PlatformOs | string;
  /** The shell's own `platform` driver hooks (app version, open link, exit, back button...). */
  driver?(shell: Shell): ShellDriverHooks;
  /** Register the shell's other drivers (storage, ...) once the page is known to be inside it. */
  register?(app: KitrApp, shell: Shell): void;
}

How a shell package configures createShellProvider().

ShellDriverHooks

type ShellDriverHooks = Partial<Omit<PlatformDriver, 'type' | 'native' | 'runtime' | 'os'>>;

The platform driver hooks a shell may implement; type, native, runtime and os are filled in by the provider.

ShellRuntime

/** The native shells a provider can wrap. */
type ShellRuntime = Exclude<Runtime, 'web'>;

The native shells a provider can wrap.

@kitr/platform/cli

displayName

function displayName(name?: string): string;

@acme/field-notes becomes Field Notes: the default window or home-screen title.

hasCommand

function hasCommand(command: string): Promise<boolean>;

Whether command --version runs and exits 0: the check behind "the Rust toolchain is not on PATH".

installCommand

function installCommand(ctx: PlatformContext, dependency: PlatformDependency): string;

Where a dependency is declared and how to install it, for the kitr doctor fix line.

lanAddress

function lanAddress(): Promise<string>;

The machine's first non-internal IPv4 address, for a device on the network to reach the dev server; 0.0.0.0 when there is none.

missingDependencies

function missingDependencies(ctx: PlatformContext, dependencies: PlatformDependency[], pkg?: Record<string, any>): PlatformFinding[];

One error finding per dependency the project's package.json does not list.

readPackage

function readPackage(ctx: PlatformContext): Record<string, any>;

Read package.json, or {} when it is missing or invalid.

shellPlatformHooks

function shellPlatformHooks<O extends ShellPlatformOptions, S = undefined>(options: O, cli: ShellCli<O, S>): PlatformHooks<O>;

The hooks of a shell platform, for the factory definePlatform takes:

definePlatform<MyOptions>(definition, (options) => shellPlatformHooks(options, { ... }))

dev starts Vite on --port (or the option), waits for it, then launches the shell; build runs the client build and then the shell's own; doctor checks dependencies, files and tools.

slug

function slug(name: string): string;

The lower-case alphanumeric form of a name, for bundle ids: Field Notes becomes fieldnotes.

waitForPort

function waitForPort(host: string, port: number, signal?: AbortSignal): Promise<void>;

waitForUrl for a host and port; a wildcard bind address (0.0.0.0) is polled on loopback.

waitForUrl

function waitForUrl(url: string, signal?: AbortSignal): Promise<void>;

Poll url until it answers (any response, even an error status) or signal aborts. Gives up after about 30 seconds.

RequiredFile

interface RequiredFile {
  /** Path from the project root. */
  path: string;
  /** The `kitr doctor` fix line. Default: `kitr platforms scaffold <name>`. */
  fix?: string;
}

A file the shell needs: reported as error when missing, with the command that recreates it.

RequiredTool

interface RequiredTool {
  /** The executable, checked with `<command> --version`. */
  command: string;
  /** The finding's message; `<name>:` is prepended. */
  message: string;
  /** The fix line. */
  fix?: string;
}

A command-line tool the shell builds with: cargo for Tauri.

ShellBuild

interface ShellBuild<O extends object> {
  /** Build the shell around `dist/<name>/client` (installers, a synced native project). */
  shell(ctx: PlatformContext<O>): Promise<void>;
  /** Runs before the client build (generate icons, create native projects). */
  before?(ctx: PlatformContext<O>): void | Promise<void>;
}

What kitr build <name> runs after the client build.

ShellCli

interface ShellCli<O extends ShellPlatformOptions, S = undefined> {
  /** npm dependencies `kitr add` installs; `kitr doctor` checks them too. */
  dependencies: PlatformDependency[];
  /** Write the shell's files. */
  scaffold(ctx: PlatformContext<O>): void;
  /** `kitr dev <name>`. */
  dev: ShellDev<O, S>;
  /** `kitr build <name>`. */
  build: ShellBuild<O>;
  /** `kitr start <name>`. Omit when the shell has nothing to start (the mobile app runs on a device). */
  start?(ctx: PlatformContext<O>): Promise<void>;
  /** `kitr doctor`. */
  doctor?: ShellDoctor<O>;
  /** Lines for the platform's `AGENTS.md` section. */
  agents: string;
}

How a shell package parameterises the platform CLI skeleton.

ShellDev

interface ShellDev<O extends object, S = undefined> {
  /** Runs before the dev server starts (create the native project, pick a target); its result goes to the other hooks. */
  prepare?(ctx: PlatformContext<O>): S | Promise<S>;
  /** The address Vite binds to. Default: Vite's own (localhost). The mobile shell passes the LAN address. */
  host?(ctx: PlatformContext<O>): string | undefined | Promise<string | undefined>;
  /** The line logged once the server's address is known. */
  banner(ctx: PlatformContext<O>, dev: ShellDevInfo<S>): string;
  /** Runs while Vite starts, before waiting for it (generate icons). */
  ready?(ctx: PlatformContext<O>, dev: ShellDevInfo<S>): void | Promise<void>;
  /** Start the shell against the dev server. Errors are logged, and the dev server keeps running until it exits. */
  launch(ctx: PlatformContext<O>, dev: ShellDevInfo<S>): Promise<void>;
}

What kitr dev <name> runs: the Vite dev server and then the shell on top of it.

ShellDevInfo

interface ShellDevInfo<S = undefined> {
  /** The port, from `--port` or the platform option. */
  port: number;
  /** The bind address, when `host()` gave one. */
  host: string | undefined;
  /** `http://<host or localhost>:<port>`. */
  url: string;
  /** What `prepare()` returned. */
  state: S;
}

What the dev hooks know about the running dev server.

ShellDoctor

interface ShellDoctor<O extends object> {
  /** Dependencies that must be in `package.json`. Default: the platform's `dependencies`. */
  dependencies?: PlatformDependency[];
  /** Files that must exist, relative to the project root. */
  files?(ctx: PlatformContext<O>): RequiredFile[];
  /** Tools that must be on `PATH`. */
  tools?: RequiredTool[];
  /** Checks of your own (a `main` field, a registered URL scheme). Appended last. */
  extra?(ctx: PlatformContext<O>, pkg: Record<string, any>): PlatformFinding[];
}

What kitr doctor checks for a shell platform.

ShellPlatformOptions

/** The options every shell platform has. */
interface ShellPlatformOptions {
  /** Dev server port the shell loads from with `kitr dev <name>`. */
  port: number;
}

The options every shell platform has.