CLI Commands Reference

Commands use the form t4l <command> [target] [flags]. Target is a platform (ios, android) or, for create, an extension type (module, element, service, combo). All flags support both short and long form (e.g. -d or --debug).


Global

On startup t4l loads .env then .env.local from the directory containing tamer.config.json (walking up from the current working directory). Values are applied only when the variable is not already set in the process environment — so CI or shell exports always win.

Android signing (referenced in tamer.config.json):

ANDROID_KEYSTORE_PATH=android/release.keystore
ANDROID_KEYSTORE_PASSWORD=...
ANDROID_KEY_ALIAS=release
ANDROID_KEY_PASSWORD=...

App Store Connect API (used with t4l build ios -p --ipa):

APP_STORE_CONNECT_API_KEY_PATH=/path/to/AuthKey.p8
APP_STORE_CONNECT_API_KEY_ID=XXXXXXXXXX
APP_STORE_CONNECT_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Optional overrides for those variable names live under ios.appStoreConnect in tamer.config.json. When all three App Store Connect vars are set, the CLI runs xcodebuild archive + -exportArchive with app-store export (distribution signing + API key auth) instead of zipping a locally signed .app.

CommandDescription
t4l or t4l initInteractive setup: detects or creates a Lynx app, installs selected Tamer packages, and writes tamer.config.json
t4l add …Add @tamer4lynx/* to the Lynx project — details below
t4l add-coreInstall the core stack in one command — details below
t4l add-devInstall dev-client and its dependencies — details below
t4l updateBump every @tamer4lynx/* listed in package.json to npm’s default installable version — details below
t4l build-dev-appBuild the Tamer Dev App for Android, iOS, or both — details below
t4l signing [platform]Configure Android/iOS signing (interactive; Android can generate a keystore with keytool)
t4l --helpShow help
t4l --versionShow version

Developing the CLI (this repository)

Do not run node index.ts — Node ESM does not resolve extensionless ./src/... imports. From the repo root: npm run cli -- <args> (runs tsx index.ts), or npm run build && node dist/index.js <args>.


t4l init

Bootstrap or configure a Tamer project with an Ink interactive wizard. It detects a root or nested Lynx app (lynx.config.* or @lynx-js/rspeedy), creates a starter when none exists, normalizes nested apps into a root workspace, installs dependencies from the root, writes tamer.config.json, injects pluginTamer() into lynx.config.* when possible, and syncs TypeScript component types. By default the iOS app name and bundle ID reuse the Android app name and package ID unless iOS customization is selected.

Default starter: Rspeedy React TypeScript + Biome. New project names are derived from the root folder name; the default native ID is com.<project_name> after identifier sanitization. Existing nested apps are written as lynxProject: "<dir>"; root apps omit lynxProject.

FlagDescription
--template <template>Starter to scaffold when no Lynx app is detected: rspeedy (React TypeScript + Biome via create rspeedy) or vue-lynx (Vue TypeScript + Biome via create vue-lynx)
--dir <path>Lynx app directory. Use . for root layout
--install <stack>Tamer packages: core, dev, or none
--pm <pm>Package manager: npm, pnpm, or bun
-y, --yesAccept defaults: Rspeedy React TypeScript + Biome, detected package manager, core packages

For TypeScript, when a tsconfig.json exists (project root or under lynxProject), init may flatten project references (TS6310), then generate .tamer/tamer-components.d.ts and ensure include for that file. Broad globs such as node_modules/@tamer4lynx/tamer-*/src/**/*.d.ts are removed when present.


t4l signing [platform]

Configure Android and iOS release signing in tamer.config.json (Ink interactive wizard).

ArgumentDescription
(none)Choose Android, iOS, or both
androidAndroid only
iosiOS only

Android: Choose Generate a new release keystore (requires a JDK: keytool on PATH or JAVA_HOME) or Use an existing keystore. Generation runs keytool -genkeypair (RSA 2048, 10000-day validity) and writes the keystore under a path you choose (default android/release.keystore). Passwords are referenced by env var names in tamer.config.json; on generate, the wizard appends ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_PASSWORD (or names you choose) to .env.local if it exists, else .env if that file exists, else it creates .env.local—each new line only; existing keys are not overwritten. The CLI loads .env then .env.local at startup (see Global). Gradle can still use an optional android/signing.properties if you maintain it yourself; the wizard does not write it.

iOS: Prompts for Development Team ID and optional code-sign identity / provisioning profile specifier.

After setup, use t4l build android -p / t4l build ios -p for production signed builds.


t4l create <target>

Create a project or extension. Target: ios | android | module | element | service | combo.

TargetDescription
iosCreate iOS project
androidCreate Android project (host by default; use -r for dev-app)
moduleCreate Lynx extension with native module only
elementCreate Lynx extension with custom element (JSX preserved)
serviceCreate Lynx extension with service only
comboCreate Lynx extension with module + element + service
FlagShortDescription
--release-rAndroid: create a release-variant project

Examples:

t4l create ios
t4l create android
t4l create android -r
t4l create module
t4l create element
t4l create combo

t4l build <platform>

Build your app. Platform is required: ios | android (one platform per command). With debug (-d), the dev client (QR scan, HMR) is embedded when @tamer4lynx/tamer-dev-client is installed; with release (-r), the build has no dev client (unsigned); with production (-p), the build is signed for app store.

Production (-p): Configure signing first (t4l signing, or t4l signing android / t4l signing ios). If signing is not set up for that platform, the CLI exits with instructions. To build both platforms, run t4l build android -p and t4l build ios -p separately.

iOS -r / -p vs dev-client: Release/production builds do not build or copy dev-client.lynx.bundle and use the plain host ViewController (main bundle only). You may still see @tamer4lynx/tamer-dev-client in the autolinker’s package list because it provides native iOS pods; that is not the same as embedding the dev-client Lynx shell.

Lynx DevTool: Debug (-d) hosts with tamer-dev-client register native DevTool hooks so you can attach the desktop Lynx DevTool over USB. -r / -p builds do not enable DevTool (iOS: #if DEBUG only; Android: debug classpath / no-op in release).

iOS -p and -i: Production (-p) always builds for real devices (iphoneos) with code signing enabled (configure via t4l signing ios). Signing settings from tamer.config.json are passed to xcodebuild. With -i, the CLI installs using xcrun devicectl device install app --device <UDID> (Xcode 15+). If multiple physical devices are connected, an interactive device picker runs; with one device, install proceeds without a prompt. Debug (-d) with -i still targets the simulator (simctl install).

Android -i: Install uses Gradle install* with ANDROID_SERIAL set when you pick a device, or a single connected device. If multiple adb devices are connected, an interactive device picker runs; with one device, install proceeds without a prompt. Launch uses adb -s <serial> shell am start ….

iOS --ipa: With t4l build ios -p (and optionally -i), --ipa produces an IPA under ios/build/ipa-export/. Without App Store Connect env vars (see Global): unsigned build, manual codesign, then zip into Payload (manual path). With APP_STORE_CONNECT_API_KEY_PATH, APP_STORE_CONNECT_API_KEY_ID, and APP_STORE_CONNECT_ISSUER_ID set (and ios.signing.developmentTeam), the CLI uses xcodebuild archive + -exportArchive with method app-store and your .p8 API key. Use -p; --ipa alone is invalid.

Non-interactive CI: If multiple devices are connected and stdin is not a TTY, install fails with a message to connect a single device.

FlagShortDefaultDescription
--embeddable-eOutput to embeddable/ for adding LynxView to an existing app. Requires t4l build android --embeddable (Android only). Runs a release-style embeddable build internally; you do not need to pass --release. When -e is present, the CLI only performs the embeddable step.
--debug-ddefaultDebug build with dev client embedded (if tamer-dev-client is installed).
--release-rRelease build without dev client (unsigned).
--production-pProduction build for app store (signed).
--install-iAndroid: install APK to device. iOS: with debug (-d), install to the booted simulator; with production (-p), install to a physical device only via devicectl (simulator is never used with -p).
--ipaiOS only (t4l build ios -p): archive and export a signed IPA after the production build.
--clean-CAndroid only: run Gradle clean before building (helps with stubborn caches). No effect on iOS-only builds.

Examples:

t4l build android
t4l build android -i
t4l build ios -r
t4l build android --release --install
t4l build android -C
t4l build ios -p -i
t4l build ios -p --ipa
t4l build android --embeddable

t4l build-dev-app

Build the tamer-dev-app — the standalone Expo Go-style dev host (QR scan, HMR). Run from the tamer-dev-app package directory or from the monorepo root; the CLI detects the dev app automatically.

FlagShortDefaultDescription
--platform <platform>-pallPlatform: android, ios, or all
--install-iInstall to connected device/simulator after building

Examples:

t4l build-dev-app
t4l build-dev-app --platform android --install
t4l build-dev-app --platform ios

Link native modules to the project and sync native dependencies. Platform: ios | android | both (optional; default both).

  • iOS: Updates Podfile and LynxInitProcessor.swift (imports, module registrations, DevClientModule.attachSupportedModuleClassNames), then runs pod install in the ios directory. If @tamer4lynx/tamer-dev-client is installed, also writes tamer-host-native-modules.json into ios/<AppName>/ (lists JVM-style moduleClassName values matching dev server meta.json) and registers it in the Xcode project’s Copy Bundle Resources.
  • Android: Updates settings.gradle.kts, app/build.gradle.kts, and generated code, then runs Gradle sync (./gradlew projects) in the android directory.
  • TypeScript / IDE: When syncTamerComponentTypes is not false in tamer.config.json, each platform’s autolink step also regenerates .tamer/tamer-components.d.ts and ensures tsconfig.json includes it (see Ambient types).
FlagShortDescription
--silent-sRun without output. Useful for CI or postinstall scripts.

Examples:

t4l link
t4l link android
t4l link --silent

Host apps (tamer-dev-app, projects created with t4l ios create): LynxInitProcessor.swift from tamer-dev-client templates only contains GENERATED IMPORTS / GENERATED AUTOLINK placeholders. Run t4l link from the app root (where tamer.config.json lives) so those sections are filled from your node_modules @tamer4lynx/* packages. In the monorepo, packages/tamer-dev-app provides npm run link:native after building the CLI (npm run build at repo root).

User-provided native modules: To ensure your custom native extensions are discovered and linked:

  1. Install the package in your workspace root package.json (or ensure it's hoisted to the root node_modules in monorepos).
  2. The package must include lynx.ext.json or tamer.json with ios and/or android configuration.
  3. Run t4l link after adding dependencies to update Podfile/Gradle and native registration code.
  4. For iOS, run pod install in the ios/ directory after podspec changes.

iOS No podspec found for a Tamer package: Often the app depended on npm install …@latest, where latest points at an older tarball without ios/. Prefer t4l add tamer-insets (or the relevant package): it resolves the default installable npm version for the package and falls back only if latest is missing. Then run t4l link ios again.

Xcode reports unknown UUID in project.pbxproj: This can occur after merge conflicts or if the project file was manually edited. Fix by reverting ios/<AppName>.xcodeproj/project.pbxproj from git and re-running t4l bundle ios or t4l link ios to regenerate resource references.


t4l bundle [platform]

Build the Lynx bundle and copy it to the native project. Platform: ios | android (optional; omit for both). Runs autolink before bundling (iOS: includes tamer-host-native-modules.json when dev-client is present — see t4l link).

FlagShortDefaultDescription
--debug-ddefaultDebug bundle with dev client embedded (if present)
--release-rRelease bundle without dev client (unsigned)
--production-pProduction bundle for app store (signed)

t4l inject <platform>

Inject tamer-host templates into an existing project. Platform: ios | android (required).

FlagShortDescription
--force-fOverwrite existing files. Without this, existing files are skipped.

Examples:

t4l inject android
t4l inject ios -f

t4l sync [platform]

Sync dev client files from tamer.config.json. Platform: android | ios | both (optional; default android).

  • Android: syncs TemplateProvider, MainActivity, DevClientManager.
  • iOS: syncs the dev client bundle and host configuration.
  • both: runs Android then iOS sync in sequence.

Note: This runs automatically during t4l bundle and t4l build. Use it manually when you've changed tamer.config.json (e.g., devServer.host or devServer.port) and want to update the generated native files without building or bundling.

Examples:

t4l sync
t4l sync android
t4l sync ios
t4l sync both

t4l start

Start the dev server with HMR and WebSocket support (Expo-like). Prints bundle URLs, meta.json, QR code, and WebSocket endpoint.

If the configured/default port is busy, t4l start tries the next port numbers until it binds successfully. The dashboard shows both the active port and a warning that the configured/default port was unavailable for this session.

Multiple bundles: set paths.lynxAdditionalBundles in tamer.config.json to an array of extra .lynx.bundle filenames under the same paths.lynxBundleRoot as paths.lynxBundleFile. Each is listed under bundles in meta.json and served from the dev server alongside the primary bundle.

Keyboard shortcuts (stdin, TTY): r rebuild, c / Ctrl+L clear screen, Ctrl+C exit.

FlagShortDescription
--verbose-vShow all logs (native + JS). Default shows JS only.

t4l add [packages...]

Add @tamer4lynx packages to the Lynx project.

ArgumentDescription
packages...Package names (e.g. tamer-auth, @tamer4lynx/tamer-auth). Bare names get @tamer4lynx/ prefix.

Examples:

t4l add tamer-auth
t4l add @tamer4lynx/tamer-auth @tamer4lynx/tamer-secure-store

t4l add-core

Adds the core stack: tamer-app-shell, tamer-screen, tamer-router, tamer-insets, tamer-transports, tamer-system-ui, tamer-icons, tamer-env (Rspeedy .env plugin). Each package is resolved to npm’s default installable version (same as t4l add / t4l add-dev). No flags.


t4l add-dev

Adds the dev stack: tamer-dev-client, tamer-linking, and the @tamer4lynx/* packages they need (app-shell, icons, insets, plugin, router, screen, system-ui). Each is resolved to npm’s default installable version (same idea as add-core / t4l add). No flags.


t4l update

Scans dependencies, devDependencies, peerDependencies, and optionalDependencies in the Lynx project’s package.json for @tamer4lynx/* names and re-installs each at npm’s default installable version (same resolution as t4l add). Skips file:, link:, portal:, and workspace: specs so local / monorepo links stay unchanged. If nothing matches, add packages first with t4l add / add-core / add-dev. No flags.

If npm reports ETARGET for @tamer4lynx/...@^0.0.n, remember that ^ on 0.0.x only allows the same patch line (e.g. ^0.0.3 is <0.0.4). A transitive dependency may still pin an older range; published @tamer4lynx/* packages use >=0.0.1-style ranges for internal deps so newer 0.0.x releases stay compatible.


After t4l add, t4l add-core, t4l add-dev, or t4l update: run t4l link so native modules are wired into iOS/Android.


t4l codegen

Generate code from @lynxmodule declarations in .d.ts files. Run from an extension package root.

No flags.


Toggle autolink on/off in tamer.config.json. When enabled, t4l link runs after installing dependencies (e.g. postinstall after npm install / pnpm install / bun install). Does not affect build or bundle commands, which always run link.

No flags.


Platform-first commands

The following form works: t4l <platform> <subcommand> [flags].

CommandDescription
t4l android createSame as t4l create android. Use -r / --release for release variant.
t4l android linkSame as t4l link android
t4l android bundleSame as t4l bundle android. Flags: -d, -r, -p.
t4l android buildSame as t4l build android. Flags: -d, -r, -p, -i, -e, -C.
t4l android syncSame as t4l sync android
t4l android injectSame as t4l inject android. Flag: -f.
t4l ios createSame as t4l create ios
t4l ios linkSame as t4l link ios
t4l ios bundleSame as t4l bundle ios. Flags: -d, -r, -p.
t4l ios buildSame as t4l build ios. Flags: -d, -r, -p, -i, --ipa, -e.
t4l ios syncSame as t4l sync ios
t4l ios injectSame as t4l inject ios. Flag: -f.

Note: Use t4l link ios or t4l link android (not --ios / --android).