npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@otalan/cli

v1.4.1

Published

Otalan CLI for bundling and publishing OTA updates through otalan.com.

Downloads

2,495

Readme

@otalan/cli

Otalan CLI for bundling and publishing OTA update releases for Capacitor and Expo apps.

Website: otalan.com

Published as an npm package, but the CLI itself runs on Bun.

Requirements

  • Bun >= 1.3.11 installed and available on your PATH
  • An Otalan OTA Publish Key for commands that talk to the Otalan API

Otalan key prefixes are stable API identifiers:

  • OTA Publish Key values use the otalan_ci_ prefix internally.
  • OTA App Key values use the otalan_ota_ prefix internally.

Do not use an OTA App Key in the CLI. OTA App Keys can be embedded in mobile app code for update checks, but they should not be shared outside the app or used for release automation.

Platform Support

The npm package ships a Bun-based CLI entrypoint, not standalone native binaries.

  • macOS and Linux are supported when Bun >= 1.3.11 is installed.
  • Windows support is experimental until the CLI release flow is validated on Windows.
  • Native compile scripts exist for macOS, Linux, and Windows maintainers, but the compiled binaries are not included in the npm package.

App Framework Support

Officially supported app targets and versions:

  • Capacitor 7 and 8 with --target capacitor
  • Expo SDK 54 and 55 with --target expo

Other app targets and older framework versions may work, but they are not officially supported for the moment.

Install

Recommended:

bun add -g @otalan/cli

If you install the package with npm, pnpm, or yarn, bun still needs to be installed because the executable runs with #!/usr/bin/env bun.

Local development from this repo:

bun ./src/bin.ts help

Quick Start

Capacitor

  1. Log in with your OTA Publish Key:
otalan login --api-key otalan_ci_xxx
  1. Link the current repo to your active Otalan app:
otalan init
  1. Build your web assets with your app's normal build command.

  2. Bundle the OTA payload:

otalan bundle --target capacitor --platform ios --bundle-id 1.0.5
  1. Publish the release:
otalan publish --channel production

otalan bundle --target capacitor packages existing built web assets. By default it reads dist/ first, then www/; pass --input-dir <path> if your build outputs somewhere else. Your app build must run first. otalan publish waits for server-side validation to finish before it returns.

Expo

  1. Log in with your OTA Publish Key:
otalan login --api-key otalan_ci_xxx
  1. Link the current repo to your active Otalan app:
otalan init
  1. Bundle the OTA payload:
otalan bundle --target expo --platform ios --bundle-id 1.0.5
  1. Publish the release:
otalan publish --channel production

otalan bundle --target expo runs bunx expo export itself, exports into a temporary project-local .otalan/expo-export-* folder, packages the exported JS bundle and assets, and stores the generated Otalan satellite manifest for publish. You do not need to create a dist/ or www/ folder before running it. otalan publish waits for server-side validation to finish before it returns.

CI/CD Usage

The CLI is designed to work well in CI/CD with a project-scoped OTA Publish Key.

Set these secrets in your CI provider:

  • OTALAN_API_KEY with your OTA Publish Key
  • OTALAN_APP_ID for an active app

Optional:

  • OTALAN_API_URL

CI/CD Example: Capacitor

bun install --frozen-lockfile
bun add -g @otalan/cli
bun run build
otalan login --api-key "$OTALAN_API_KEY" --api-url "${OTALAN_API_URL:-https://api.otalan.com}"
otalan init --app-id "$OTALAN_APP_ID"
otalan bundle --target capacitor --platform ios --bundle-from-package
otalan publish --channel production

Use your normal app build command before otalan bundle. The CLI then packages the built web output from dist/ or www/ by default; pass --input-dir <path> if your Capacitor web output uses another folder.

CI/CD Example: Expo

bun install --frozen-lockfile
bun add -g @otalan/cli
otalan login --api-key "$OTALAN_API_KEY" --api-url "${OTALAN_API_URL:-https://api.otalan.com}"
otalan init --app-id "$OTALAN_APP_ID"
otalan bundle --target expo --platform ios --bundle-from-package
otalan publish --channel production

This runs bunx expo export through the CLI, using a temporary project-local .otalan/expo-export-* folder, packages the exported OTA assets, and publishes the resulting bundle through Otalan's validation pipeline. Do not add a separate web build step just to create dist/ or www/ for Expo.

GitHub Actions Example

name: Publish OTA

on:
  workflow_dispatch:

jobs:
  publish-ios:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: 1.3.11

      - run: bun install --frozen-lockfile
      - run: bun add -g @otalan/cli
      - run: bun run build
      - run: otalan login --api-key "$OTALAN_API_KEY"
        env:
          OTALAN_API_KEY: ${{ secrets.OTALAN_API_KEY }}
      - run: otalan init --app-id "$OTALAN_APP_ID"
        env:
          OTALAN_APP_ID: ${{ secrets.OTALAN_APP_ID }}
      - run: otalan bundle --target capacitor --platform ios --bundle-from-package
      - run: otalan publish --channel production

Adjust the build step and bundle target for your app:

  • Capacitor: keep your web build step and use --target capacitor
  • Expo: remove the web build step if not needed and use --target expo

What It Does

  • logs into the Otalan API
  • checks API connectivity and OTA Publish Key context
  • generates OTA Publish Key and OTA App Key material locally for dashboard import
  • links the current repo to an Otalan app
  • bundles Capacitor or Expo OTA output
  • publishes a bundle with rollout metadata
  • lists published bundles
  • rolls back to an older bundle
  • shows current bundle status

The CLI supports one release write path: otalan publish. There is no separate upload command.

Config Files

Global auth config:

~/.otalan/config.json

Project config:

otalan.config.json

Example project config:

{
  "organizationSlug": "example-organization",
  "projectSlug": "example-project",
  "appName": "Example App",
  "appId": "com.example.app"
}

otalan.config.json only links the repo to an Otalan project/app. Bundle and release targeting data such as target, platform, runtimeVersion, and bundleId live in .otalan/bundle/manifest.json.

Command Reference

otalan help

Shows the available commands and usage notes. Running otalan without arguments prints the same concise command list and notes.

otalan version

Prints the installed CLI version.

otalan version
otalan --version
otalan -v

otalan keygen

Generates Otalan key material locally without calling the API. Use this for workflows where a team wants to create the key in its own terminal, CI setup, or secrets manager before importing it in the Otalan dashboard.

otalan keygen --kind ci
otalan keygen --kind ota

If --kind is omitted, the CLI prompts for OTA Publish Key or OTA App Key.

The --kind values and generated key prefixes keep the existing internal API identifiers:

  • --kind ci generates an OTA Publish Key with the otalan_ci_ prefix.
  • --kind ota generates an OTA App Key with the otalan_ota_ prefix.

Output includes both the full Otalan key and the base64url suffix without the otalan_ci_ or otalan_ota_ prefix:

Generated OTA Publish Key.

Full key:
otalan_ci_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Key without prefix:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

otalan keygen only creates local key material. Importing or activating a key should still happen through an authenticated dashboard flow; an existing OTA Publish Key should not be able to create more keys. OTA App Keys are intended for embedded app update checks and should not be shared or used as CLI credentials.

otalan login

Saves the project OTA Publish Key and API base URL locally.

If auth is already saved, otalan login shows the current API URL as the prompt default and shows the current OTA Publish Key in masked form. Press Enter to keep either value.

otalan login --api-key otalan_ci_xxx --api-url https://api.otalan.com

otalan doctor

Checks API connectivity and prints the organization/project context resolved from the configured OTA Publish Key.

otalan doctor

CI usage without saved local auth:

otalan doctor --api-key "$OTALAN_API_KEY" --api-url "${OTALAN_API_URL:-https://api.otalan.com}"

otalan init

Creates otalan.config.json in the current project.

otalan init lists the active apps in the project resolved from the logged-in OTA Publish Key and lets you select one. appId is scoped to that project, not globally unique across all projects. Archived apps are not listed and are treated as unavailable for CI publish, rollback, status, and bundle listing commands.

Run otalan init once per app repo or working folder. If you switch to another checkout, folder, or app project, run otalan init there too so that folder has its own otalan.config.json.

If you pass --app-id, the CLI validates that the app exists in the logged-in project before writing otalan.config.json. The CLI also stores organizationSlug, projectSlug, and the selected app name as a safety check and display context.

otalan init
# Non-interactive CI usage:
otalan init \
  --app-id com.example.app

otalan bundle

Builds .otalan/bundle/bundle-<bundle-id>.zip and .otalan/bundle/manifest.json.

.otalan/ is generated output. Add it to your app repo's .gitignore; otalan publish reads the bundle files from the current CI workspace after otalan bundle runs.

Capacitor:

otalan bundle --target capacitor --platform ios
# Custom Capacitor web output folder:
otalan bundle --target capacitor --platform ios --input-dir build

Expo:

otalan bundle --target expo --platform ios

Current behavior:

  • Official support covers Capacitor 7 and 8, and Expo SDK 54 and 55
  • Other app targets and older framework versions may work, but they are not officially supported for the moment
  • Capacitor packages prebuilt web assets; it does not run your app build command
  • without --input-dir, Capacitor checks dist/ first and then www/
  • pass --input-dir <path> to package a different Capacitor web output folder
  • Expo runs bunx expo export --platform <platform> into a temporary project-local .otalan/expo-export-* folder
  • Expo does not require a prebuilt dist/ or www/ folder
  • Expo stores the generated Otalan satellite manifest in .otalan/bundle/manifest.json, including launchAsset, assets, runtimeVersion, bundleId, and expoConfig
  • both outputs produce a ZIP plus manifest.json
  • source map files (*.map) are omitted from bundle ZIPs by default; the CLI prints the omitted file count when any are skipped
  • native project/source files are rejected before bundle output is written; OTA bundles must only contain generated web/update assets
  • when otalan.config.json is available, the CLI prints the linked project and app before packaging
  • when otalan login and otalan init are configured, the CLI checks that the selected bundleId is not already published for the selected platform, runtimeVersion, and channel before writing bundle output
  • --platform is required so the CLI exports the selected platform and resolves the correct runtime version

Runtime version defaults:

  • In an interactive terminal, otalan bundle prompts for the runtime version after showing the detected active runtime version.
  • Capacitor iOS defaults runtimeVersion from CFBundleShortVersionString in Info.plist and resolves $(MARKETING_VERSION) from the Xcode project when needed
  • Capacitor Android defaults runtimeVersion from versionName in android/app/build.gradle or build.gradle.kts
  • Expo runtimeVersion reads --runtime-version, Expo export metadata, or Expo config runtimeVersion policies/strings; if none are present, the CLI falls back to the selected platform Expo version
  • --runtime-version overrides auto-detection

Native project file parsing is best-effort. If your Info.plist, Xcode build settings, or Gradle files use patterns the CLI cannot read, pass --runtime-version explicitly.

For Expo projects, the recommended app config is:

{
  "expo": {
    "version": "1.0.0",
    "runtimeVersion": {
      "policy": "appVersion"
    }
  }
}

Use a string value instead if you manage runtime compatibility manually:

{
  "expo": {
    "runtimeVersion": "1.0.0"
  }
}

Choose the bundle ID you want to release:

otalan bundle --target capacitor --platform ios --bundle-id 1.0.5
otalan bundle --target expo --platform ios --bundle-id 1.0.5

bundleId is the customer-facing OTA release identifier for all targets. The CLI maps it to the target-specific metadata internally.

If you omit bundleId:

  • in an interactive terminal, the CLI prompts for a bundle ID and shows the local bundle ID from .otalan/bundle/manifest.json when available
  • when otalan login and otalan init are configured, the prompt also shows the active published bundle ID for the selected platform/runtime version/channel
  • published bundle hints use --channel, defaulting to production
  • duplicate published bundle ID checks use the same --channel value and default to production
  • pressing Enter without a bundle ID keeps the automatic bundle ID behavior
  • the CLI reads runtimeVersion from the selected platform and adds a short hash suffix
  • example: 1.0.0-abc123def456

If you want to take the bundle ID from package.json instead:

otalan bundle --target capacitor --platform ios --bundle-from-package
otalan bundle --target expo --platform ios --bundle-from-package

otalan publish

Publishes the current bundle output with rollout metadata.

otalan publish uses the bundleId, platform, and runtimeVersion stored in .otalan/bundle/manifest.json. To release 1.0.5, set it when you run otalan bundle --bundle-id 1.0.5.

Current behavior:

  • channel is chosen at publish time
  • publishes are mandatory by default
  • default rollout is 100
  • --platform and --runtime-version can override the manifest, but only if they match it
  • --output-dir lets you publish a bundle from a non-default folder
  • --rollout-percent accepts an integer from 0 to 100
  • --optional marks the update as non-mandatory
  • --release-notes attaches release notes to the published bundle
  • Expo publish forwards the full generated Otalan satellite manifest when present
  • Expo publish sends the generated manifest with runtimeVersion
  • Expo manifests include the Expo config captured from bunx expo config --json; avoid placing secrets in Expo config fields that are not intended to be uploaded
  • Otalan validates the release ZIP before the publish completes
  • active rollouts can be paused and resumed later without changing the selected bundle

Default flow:

otalan publish --channel production

Staged rollout:

otalan publish --channel production --rollout-percent 25 --release-notes "Fix startup crash"

Optional update:

otalan publish --channel production --optional

This uses the direct-upload release flow:

  1. POST /v1/releases/create with JSON metadata for the release and local ZIP, including expoManifest for Expo bundles
  2. PUT the ZIP bytes directly to the returned opaque uploadUrl with the exact returned uploadHeaders, including Content-Length
  3. POST /v1/releases/ingests/:id/complete
  4. poll GET /v1/releases/ingests/:id until the ingest reaches ready or failed

If the direct object-storage upload fails before completion, the CLI calls POST /v1/releases/ingests/:id/cancel so the reserved ingest does not block a retry.

The ZIP is opened as a disk-backed Bun.file and passed directly to the returned PUT upload URL; otalan publish does not load the full archive into memory first.

If validation fails, otalan publish exits non-zero and prints the ingest failure reason when the API provides one. This makes the command safe to use directly in CI/CD pipelines.

otalan bundles

Lists remote bundles for the current app so you can choose a bundle for rollback or rollout operations.

Remote bundle tables display the API publishedAt timestamp, not the bundle row createdAt timestamp.

Default resolution order:

  1. --runtime-version
  2. .otalan/bundle/manifest.json if present and the manifest platform matches the selected platform
  3. runtime version derived from the selected platform in the local app project
  4. interactive prompt
otalan bundles --platform ios --channel production

otalan rollback

Reactivates an older bundle for the same tuple.

rollback uses the same runtime-version default order as bundles. Pass --runtime-version if you want to override the detected default.

otalan rollback --bundle-id 1.0.0-web.1 --platform ios --channel production

otalan pause

Pauses delivery of the currently active bundle for the selected release tuple.

pause uses the same runtime-version default order as bundles. The active bundle remains selected, but new OTA checks stop receiving it until you resume the rollout.

otalan pause --platform ios --channel production

otalan resume

Resumes delivery of the currently active bundle for the selected release tuple.

resume uses the same runtime-version default order as bundles.

otalan resume --platform ios --channel production

otalan status

Shows the active bundle for the selected release tuple.

The active bundle summary displays publishedAt as Published at.

status also uses the same runtime-version default order as bundles.

otalan status --platform ios --channel production

Bundle Output

Capacitor Manifest

{
  "target": "capacitor",
  "hash": "sha256...",
  "runtimeVersion": "1.0.0",
  "bundleId": "1.0.0-abcdef123456",
  "createdAt": "2026-04-07T12:00:00.000Z",
  "platform": "ios"
}

Expo Satellite Manifest

{
  "target": "expo",
  "hash": "sha256...",
  "runtimeVersion": "1.0.0",
  "bundleId": "1.0.0-abcdef123456",
  "launchAsset": "bundles/ios-xxxxx.js",
  "assets": [
    "assets/asset_1.png"
  ],
  "expoConfig": {
    "name": "Example",
    "slug": "example",
    "scheme": "example"
  },
  "createdAt": "2026-04-07T12:00:00.000Z",
  "platform": "ios"
}

For Expo publishes, otalan publish serializes this file and sends it to /v1/releases/create as expoManifest.

Maintainer Release Checklist

Before publishing a public package release:

  • update package.json to the new package version
  • add the matching CHANGELOG.md entry
  • run the release checks and inspect the package dry run
bun install --frozen-lockfile
bun test
bun run check
bun run lint
bun run build
bun pm pack --dry-run

prepublishOnly reruns tests, TypeScript, and ESLint. The scoped npm package is configured for public publishing through publishConfig.access.

Notes

  • This is a Bun-based CLI published on npm.
  • Expo bundling uses bunx expo ....
  • Default API URL is https://api.otalan.com.
  • Publishing, rollback, status, and bundles expect an OTA Publish Key and an active app.
  • Bundle and release commands print the linked project and app before continuing when project config is available.
  • Run bun run build after changing CLI source if you want dist/bin.js updated locally.