Using CLI Plugin
The build toolchain for a sigx-lynx app: a dev server, native prebuild, autolinking and device runners — driven from the command line, configured from a single TypeScript file.
Basic usage
@sigx/lynx-cli is mostly a command-line tool, not a library you import. It registers as a plugin for @sigx/cli (auto-discovered via the package's sigx-cli.plugin field), which adds the sigx subcommands to your project.
Install both packages into a Lynx app:
pnpm add -D @sigx/cli @sigx/lynx-cli@sigx/lynx-cli requires @sigx/cli ≥ 0.4 — it builds on the 0.4 plugin API (typed args via @sigx/cli's re-exported a builders, and the dashboard shell).
A project is detected as a Lynx project when signalx.config.ts (or .js/.mjs) or lynx.config.ts exists at its root. From there, the day-to-day commands are:
sigx dev # start the dev server, pick a device
sigx prebuild # render native ios/ and android/ projects + autolink
sigx run:android # build, install and launch on Android
sigx run:ios # build, install and launch on iOS (macOS only)
sigx run:web # build, serve and live-reload in the browser
sigx build # production rspeedy bundle
sigx doctor # environment health report
The JavaScript API (defineLynxConfig, runPrebuild, resolveConfig, the device-detect helpers, etc.) exists for tooling that needs to drive the same machinery programmatically, but most apps only ever touch defineLynxConfig.
Configuring your app
Everything about the native shell — name, icons, splash, permissions, deep-link scheme, native modules — lives in signalx.config.ts. Wrap the object in defineLynxConfig for full typing and editor completion. It is a pure identity helper, so it has no runtime effect beyond typing.
// signalx.config.ts
import { defineLynxConfig } from '@sigx/lynx-cli/config';
export default defineLynxConfig({
name: 'My App',
version: '1.0.0',
scheme: 'myapp',
orientation: 'portrait',
icon: 'assets/icon.png',
splash: {
image: 'assets/splash.png',
backgroundColor: '#0D9488',
dark: { backgroundColor: '#134E4A' },
},
modules: [
{ package: '@sigx/lynx-location', config: { accuracy: 'high' } },
],
iconSets: [
{ id: 'fa', source: '@sigx/lynx-icons-fa-free', styles: ['solid', 'brands'] },
{ id: 'lucide', source: '@sigx/lynx-icons-lucide' },
],
android: {
applicationId: 'com.mycompany.myapp',
versionCode: 2,
adaptiveIcon: {
foreground: 'assets/adaptive-foreground.png',
backgroundColor: '#0D9488',
monochrome: 'assets/adaptive-monochrome.png',
},
notificationIcon: 'assets/notification-icon.png',
notificationColor: '#0D9488',
features: [{ name: 'android.hardware.camera', required: false }],
},
ios: {
bundleIdentifier: 'com.mycompany.myapp',
buildNumber: '2',
icon: { light: 'assets/icon.png', dark: 'assets/icon-dark.png', tinted: 'assets/icon-tinted.png' },
supportsTablet: true,
requiresFullScreen: false,
},
});
Notes:
- Installed
@sigx/lynx-*modules are autolinked automatically — you only list a module inmoduleswhen you need to pass it native config or restrict it to one platform. UseexcludeModulesto opt out of one. - The config file is evaluated at prebuild time, so you can read
process.envfor secrets likeandroid.googleMapsApiKey. - Sensible defaults are applied for everything you omit:
version1.0.0,buildNumber1,orientationportrait, both platforms, AndroidminSdk24 /targetSdk35, iOSdeploymentTarget15.0,supportsTablettrue. Missing icon or splash images fall back to bundled placeholders. - When
applicationId/bundleIdentifierare omitted, run and dev commands fall back tocom.sigx.<name>(lowercased, non-alphanumerics stripped). - App icons take appearance variants.
ios.iconaccepts a{ light, dark, tinted }set for iOS's standard / dark / tinted home-screen modes (a string is shorthand forlight). Android'sadaptiveIcontakesforeground+backgroundColor+ an optionalmonochromelayer (themed icons), andnotificationIcon/notificationColorstyle the status-bar notification glyph.splashlikewise takes adarkvariant. - Device support.
ios.supportsTablettoggles iPad support (Xcode's device family);ios.requiresFullScreenopts the iPad out of multitasking — only then may it follow the phone's orientation lock, otherwise iPad apps must allow all four orientations.android.featuresemits<uses-feature>manifest entries ({ name, required }) to declare hardware your app uses. - Cleartext HTTP is debug-only. Prebuild emits an Android network-security config that blocks cleartext (non-HTTPS) traffic in release builds and permits it only in the debug source set, so use
https://for production endpoints.
Native config passthrough
Most native settings have a dedicated config field, but when an SDK needs a key the CLI doesn't model directly, typed escape hatches merge arbitrary values into the rendered native projects on every prebuild — so a custom key survives without post-prebuild patching:
ios.infoPlist— arbitraryInfo.plistkeys (Record<string, PlistValue>; values may be scalars, arrays, or nested dicts) merged over the generated plist (last-write-wins).ios.usesNonExemptEncryption— a boolean convenience for the near-universalITSAppUsesNonExemptEncryptionkey; setfalsefor apps using only exempt encryption (e.g. HTTPS) to clear App Store Connect's "Missing Compliance" prompt. An explicitinfoPlist.ITSAppUsesNonExemptEncryptionwins if both are set.android.applicationAttributes— arbitrary attributes merged onto the<application>element inAndroidManifest.xml(Record<string, string | boolean | number>, e.g.{ usesCleartextTraffic: false, largeHeap: true }). Theandroid:namespace prefix is added automatically; booleans/numbers are stringified. It's the<application>-attribute counterpart tomanifestMetaData(which only adds<meta-data>children).
export default defineLynxConfig({
name: 'My App',
ios: {
usesNonExemptEncryption: false,
infoPlist: { LSApplicationQueriesSchemes: ['tel', 'mailto'] },
},
android: {
applicationAttributes: { largeHeap: true },
},
});
Each of these merges with values contributed by linked native modules — modules declare the same keys in their signalx-module.json, and the app-level entries win on a name collision.
Firebase and entitlements
Two more passthroughs wire the signed-capability plumbing that remote push (and anything needing an entitlement) depends on:
android.googleServicesFile— path to a Firebasegoogle-services.json. Prebuild copies it toandroid/app/google-services.jsonon every run (so it survives the managedandroid/re-render) and applies thecom.google.gms.google-servicesGradle plugin. Point it at a gitignored path to keep credentials out of source control. This is what@sigx/lynx-notificationsneeds for FCM.ios.entitlements— arbitrary app-level code-signing entitlements (Record<string, PlistValue>), the counterpart toios.infoPlist. Setting any entitlement makes prebuild generate<App>.entitlements(Release) +<App>.debug.entitlements(Debug) and wireCODE_SIGN_ENTITLEMENTSper build config.aps-environmentis special-cased — written asproductionin the Release file anddevelopmentin the Debug file regardless of the value you pass.
export default defineLynxConfig({
name: 'My App',
android: { googleServicesFile: './firebase/google-services.json' },
ios: { entitlements: { 'aps-environment': 'development' } },
});
Native modules contribute the same two mechanisms through their signalx-module.json — android.gradlePlugins and ios.entitlements (see the manifest types). So installing @sigx/lynx-notifications adds the FCM Gradle plugin and the aps-environment entitlement automatically; you only supply the Firebase file and your iOS signing. App-level entries win over a module's on a name collision.
Build variants
A variants map lets a dev/staging/preview build install alongside production with its own app id, name and output dir, selected with --variant <name>. See the dedicated Build variants guide.
Running the dev server
sigx dev starts the rspeedy dev server, probes for a free port (default 8788), and — in an interactive terminal — renders a full-screen dev dashboard: live status (port, build id, selected targets), the QR code and LAN-IP URLs for loading the bundle on a device, streaming device logs, and keyboard shortcuts / slash-commands. (In a non-TTY environment it falls back to plain banner output.)
sigx dev # interactive target picker (TTY)
sigx dev --all # build for every connected target
sigx dev --ios # iOS only
sigx dev --android # Android only
sigx dev --last # reuse the last selected target
sigx dev --port 9000 # custom port
sigx dev --variant dev # run a build variant (see Build variants)
sigx dev --no-device-logs # don't stream device console.* output
Behavior worth knowing:
- The first
devrun with noandroid/orios/folder auto-runsprebuildfor you. - In a TTY it shows an interactive target picker and remembers your last choice (
--lastreuses it). In a non-TTY environment it behaves like--all. - Device
console.*logs stream back to your terminal; selected targets are auto-launched with the dev URL. On-device runtime exceptions (the native red-screen errors) also surface in the terminal's Logs tab — see device runtime exceptions. - An app with no
lynx.config.ts(a bundle-loader client such as sigx-lynx-go) skips the JS dev server.
Prebuilding native projects
sigx prebuild renders the managed ios/ and android/ native projects from templates, autolinks every installed @sigx/lynx-* module (generating the native registries, Podfile entries, Gradle deps, manifest permissions and so on), then runs your prebuild.post hook.
sigx prebuild # both platforms
sigx prebuild --android # Android only
sigx prebuild --ios # iOS only
sigx prebuild --clean # bypass the fingerprint fast-path, full re-render
sigx prebuild --variant dev # render the dev variant into android-dev/ + ios-dev/
Prebuild has a fingerprint-based fast path: it skips work when its inputs are unchanged. Editing signalx.config.ts or the post-hook script re-triggers it; --clean forces a full re-render regardless.
Because prebuild re-renders managed native files pristine each time, a prebuild.post hook that patches them must anchor on template text and fail loudly when the anchor is missing:
// signalx.config.ts excerpt
export default defineLynxConfig({
name: 'My App',
prebuild: { post: './scripts/native-patches.mjs' },
});
The post hook's default export, if a function, is awaited with { cwd, config, platforms }.
To drive prebuild programmatically from your own tooling, call runPrebuild:
import { runPrebuild } from '@sigx/lynx-cli';
await runPrebuild({ ios: true, android: true, clean: true });
Building and running on a device
sigx run:android # debug build, install, launch
sigx run:android --release # release: bundle -> prebuild -> gradle installRelease
sigx run:ios --simulator "iPhone 15" # boot a named simulator
sigx run:ios --device "My iPhone" # a physical device (Xcode 15+ / devicectl)
sigx run:ios --release
sigx run:android --variant dev # install the dev variant alongside production
sigx build --analyze # production bundle + size summary
Platform notes:
run:ios(and any iOS build) is macOS only and errors elsewhere.- Android needs the Android SDK + JDK + Gradle;
adbis resolved fromANDROID_HOME/platform-toolsandPATH. iOS needs Xcode, CocoaPods andxcrun/devicectl. - Run
sigx doctorfirst to confirm your toolchain: it reports Node, your package manager, the Android SDK/JDK/ADB, iOS Xcode/CocoaPods and connected devices.
Code splitting
Dynamic import() works everywhere — sigx dev, release, and standalone/store
builds alike (earlier releases only split in dev). Split a heavy or rarely used
feature into its own async chunk and load it on demand:
// loaded only when the user opens the editor
button.onTap(async () => {
const { openEditor } = await import('./editor');
openEditor();
});
sigx build prints an async-chunk summary alongside the main bundle. Two things to
know before shipping:
- OTA updates carry only
main.lynx.bundle, sosigx updates:publishrefuses while async chunks are present. Host the chunks yourself (output.assetPrefix) and pass--allow-async-chunks, or skip splitting for OTA-first apps — see Publishing an update. - Standalone builds embed the chunks into the native app so they resolve offline; no extra wiring.
Running on the web
sigx run:web is the browser peer of run:android / run:ios: it builds your app for the web target and serves it locally with live reload, so you can iterate on layout and logic without a simulator or device.
sigx run:web # build, serve and open the browser (default port 8900)
sigx run:web --port 9000 # custom port
sigx run:web --no-open # don't auto-open a browser
sigx run:web --no-watch # build and serve once, no live reload
sigx run:web --host # expose on the LAN
It serves three things over one cross-origin-isolated HTTP server: a generated <lynx-view> host page, the upstream @lynx-js/web-core browser engine, and your app's own dist/ bundle. Enable the web target by adding it to your build environments:
// lynx.config.ts
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginSigxLynx } from '@sigx/lynx-plugin';
export default defineConfig({
plugins: [pluginSigxLynx()],
environments: { lynx: {}, web: {} },
});
The generated host page wires the view with @sigx/lynx-web-host: installSigxWebHost derives the web-core browser-config from the <lynx-view> element's box (opt out with { viewport: false }), and the public viewportBrowserConfig(view) exposes that computation for a hand-written host page. Set browser-config inline while the page parses, before the engine upgrades <lynx-view> — SystemInfo is frozen at view construction, so later resizes aren't tracked.
Two things to know about the web target:
- Live reload is a full reload, not hot-swap — a file change rebuilds the bundle and the page re-fetches it. Lynx's HMR transport is device-oriented and stays off for the web target (matching upstream).
- Gestures work in the browser. Tap, long-press and pan map to pointer input, so
Pressable,Draggable, sheets and swipe-to-reveal all behave on web;useAnimatedStyleanimations fall back to inline styles.
Keeping the module family in sync
The @sigx/lynx-* packages are versioned in lockstep, so the CLI ships commands to manage them as a set:
sigx add @sigx/lynx-camera # install a module (autolinks on next prebuild)
sigx remove @sigx/lynx-camera # uninstall
sigx outdated # report available updates / lockstep drift
sigx upgrade --to latest # bump the whole family together
sigx upgrade --dry-run # preview without writing
outdated exits non-zero on lockstep drift, and on an available update for the default latest check. upgrade writes exact pins by default (--caret for caret ranges) and refuses to run on a dirty git tree unless you pass --force.
How commands declare their flags
Every sigx command's flags are declared with @sigx/cli's fluent arg builders (re-exported as a from @sigx/cli/plugin, backed by @sigx/args). A command's args shape maps each flag to a typed builder, and the parsed values arrive on ctx.args:
import { a } from '@sigx/cli/plugin';
const dev = {
description: 'Start the dev server',
args: {
port: a.number().default(8788),
device: a.string().alias('d'),
deviceLogs: a.boolean().negatable(true), // --no-device-logs forces false
targets: a.rest(), // trailing positional tokens
},
run: async (ctx) => { /* ctx.args is fully typed */ },
};
Behavior worth knowing for anyone authoring a command (or a sibling plugin):
- Unknown flags error by default — a mistyped flag fails fast rather than being silently ignored.
--no-<name>negation is on by default for boolean flags (.negatable(false)to opt out).a.rest()collects trailing positional tokens asstring[]; refiners like.required(),.default(v),.multiple(), and.alias('d')set requiredness, defaults, repeat behavior, and short aliases.
This is what types the --flag columns in the command table.
See also
- Overview — what the plugin is and how it fits the family.
- Installation — project setup.
- Build variants — per-environment app identity with
--variant. - API reference — the full typed surface.
- Your first app — an end-to-end walkthrough.
