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):
App Store Connect API (used with t4l build ios -p --ipa):
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.
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.
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).
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.
Examples:
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.
Examples:
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.
Examples:
t4l link [platform]
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 runspod installin theiosdirectory. If@tamer4lynx/tamer-dev-clientis installed, also writestamer-host-native-modules.jsonintoios/<AppName>/(lists JVM-stylemoduleClassNamevalues matching dev servermeta.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 theandroiddirectory. - TypeScript / IDE: When
syncTamerComponentTypesis notfalseintamer.config.json, each platform’s autolink step also regenerates.tamer/tamer-components.d.tsand ensurestsconfig.jsonincludes it (see Ambient types).
Examples:
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:
- Install the package in your workspace root
package.json(or ensure it's hoisted to the rootnode_modulesin monorepos). - The package must include
lynx.ext.jsonortamer.jsonwithiosand/orandroidconfiguration. - Run
t4l linkafter adding dependencies to update Podfile/Gradle and native registration code. - For iOS, run
pod installin theios/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).
t4l inject <platform>
Inject tamer-host templates into an existing project. Platform: ios | android (required).
Examples:
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 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.
t4l add [packages...]
Add @tamer4lynx packages to the Lynx project.
Examples:
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.
t4l autolink-toggle / t4l autolink
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].
Note: Use t4l link ios or t4l link android (not --ios / --android).
