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

@firetower/cli

v0.14.0

Published

Install, upgrade and inspect a Firetower deployment.

Readme

@firetower/cli

Install, upgrade and inspect a Firetower deployment.

npm i -g @firetower/cli
firetower install --domain firetower.example.com

What it does

firetower install checks the machine before it writes anything, asks a handful of questions, generates the two secrets you would otherwise generate by hand, and brings the stack up. It prints the administrator's password once and makes you acknowledge the root key, because that key is the only unrecoverable thing here.

firetower upgrade pulls, recreates, waits for health — and then tells you which of your machines are still running an older worker, naming each one and the command to fix it. The control plane already compares its version against every worker's on each handshake; this asks it, and turns the answer into something to paste.

How people will reach it

The first question, because the rest of the install follows from it:

| Answer | Caddy listens on | DNS records point at | | --- | --- | --- | | Tailscale or another mesh VPN | the tailnet address, detected | the same address | | Advanced — an IP you type | what you type, or 0.0.0.0 | what you type |

Both obtain a Let's Encrypt certificate over DNS-01, and neither needs this machine to be reachable from the internet to get one.

There is no loopback install any more. It was the default for a year, and what it needed — firetower tunnel, one forward per person — stopped being a reasonable thing to ask of a team. A deployment that already has that shape keeps upgrading; it is only the choice that is gone, and firetower domain moves one onto a name.

Tailscale or another mesh VPN

The one to want, and the one where this CLI can be sure of the answer: a tailnet address is always configured on an interface of this machine, so there is nothing to type and nothing to get wrong.

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

Disable key expiry for the machine in the Tailscale admin console while you are there. Node keys lapse after 180 days by default, and a server that drops off the tailnet is a Firetower nobody can reach, with nothing to say why.

Then:

firetower install --domain firetower.example.com \
  --dns-provider cloudflare --dns-token "$CLOUDFLARE_TOKEN"

Interactively it detects the address and confirms it. WireGuard, ZeroTier, Nebula and anything on a tun/utun device are recognised too — it is not Tailscale specifically, it is whatever looks like a network other people are also on.

Advanced — an IP you type

For a public address, a LAN, or a VPN whose interface nothing here recognises. You type the address people will reach it on, and nothing is checked:

firetower install --domain firetower.example.com \
  --dns-provider cloudflare --dns-token "$TOKEN" \
  --https-bind 34.79.12.180

Nothing is checked because nothing can be. On a Google Cloud VM the only address the guest holds is something like 10.128.0.2 — an RFC1918 address that the entire internet reaches through an external IP configured outside the VM. Calling that "private" would be a reassurance about a public deployment, and the reverse case exists too: a routable address behind a firewall that answers nobody. So the CLI states the consequence once, before the prompt, and believes the answer.

The consequence, in full, is that on a publicly reachable address these are the front door:

  • the control plane, behind the login page — and it holds every git token, every agent credential and the root key;
  • every preview, behind nothing at all. A preview hostname carries its own signature and that signature is the only thing in front of it.

A mesh VPN avoids both, which is why it is the recommendation rather than a default somebody can talk themselves out of.

Behind NAT, a floating IP, or a load balancer

Google Cloud, AWS and Azure each implement an external address as NAT outside the guest, so the machine is reached at an address it does not have. Caddy cannot listen on an address that is not there, so the two become separate facts:

firetower install --domain firetower.example.com \
  --dns-provider cloudflare --dns-token "$TOKEN" \
  --https-bind 0.0.0.0 --advertise 34.79.12.180

--https-bind is what Caddy listens on; --advertise is what the DNS records point at, and what firetower doctor checks them against. Interactively you are asked for the second one only when the first cannot be it — the prompt comes prefilled with 0.0.0.0.

Hetzner, DigitalOcean, Vultr, Linode and bare metal all configure the public address on the interface itself, so none of them need this.

Both of them need the two records

firetower.example.com     A   100.69.206.104
*.firetower.example.com   A   100.69.206.104

Pointing at whichever address you settled on. The wildcard is not optional: previews are served at <session>-<port>-<signature>.your-domain, so a deployment with the apex record alone gets an interface that works and previews that do not resolve.

On a mesh those records are public and resolve for everybody — they simply only answer for people on your tailnet.

install prints them with your address filled in and waits for you to say they exist, and firetower doctor probes a random label under the domain afterwards, to tell a wildcard record apart from a single one that happens to exist.

Why DNS-01, and what comes with it

The usual ACME challenges have Let's Encrypt connect to you, and a machine Let's Encrypt can reach is a machine anyone can reach. DNS-01 proves control the other way round: Caddy writes a TXT record through your provider's API and the authority reads it back out of DNS. Every connection is outbound.

It is also the only challenge that can issue a wildcard, which this needs twice over: previews are served on subdomains, and one wildcard keeps every preview hostname out of the public Certificate Transparency logs — where a hostname that is the credential for that preview does not belong.

That is why a public deployment needs a DNS provider token exactly as much as a private one does. Going public does not simplify the install; the wildcard is what requires DNS-01, and previews are what require the wildcard.

Four things go with it:

  1. DNS_PROVIDER is compiled into Caddy. Caddy resolves DNS providers as compiled-in modules, so the tls profile builds its own image. The first up pulls a Go toolchain and takes a few minutes rather than seconds, and needs a reachable Go module proxy at that moment. It is cached afterwards.
  2. install writes COMPOSE_PROFILES=tls, which is what creates the Caddy container at all. Without it there is no proxy.
  3. Renewal is Caddy's, unattended, at about two-thirds of the certificate's life. firetower doctor reports the expiry and says who is responsible for it.
  4. Slow providers. Some serve a record minutes after their API accepts it, and Caddy asks Let's Encrypt to validate within seconds — so the challenge fails with No TXT record found for a record that was written successfully, which reads like a bad token. GoDaddy is the measured case: a wildcard failed four times at 12-17 seconds and succeeded at 124. install writes propagation_delay, propagation_timeout, dns_ttl and resolvers into the Caddyfile for the providers known to need it, so there is nothing to do. For one that is not on that list, the file says which lines to add.

Every module under github.com/caddy-dns works — all ninety-odd of them — and the CLI knows their names. The interactive prompt lists the dozen that take a single API token and lets you type any of the rest; both the prompt and --dns-provider reject a name that is not one of them, and suggest the closest:

$ firetower install --domain ft.example.com --dns-provider cloudflares
error: option '--dns-provider <module>' argument 'cloudflares' is invalid.
       no caddy-dns module called cloudflares — did you mean cloudflare?

That check earns its keep because the value is compiled in: an unchecked typo does not fail at start-up with a bad credential, it fails several minutes into a Go build, after every other question has been answered.

A full module path — github.com/libdns/something — is always accepted, for a provider that is not under caddy-dns or one added since your CLI was published.

One caveat worth stating plainly: the CLI validates provider names, not that a module currently compiles. A caddy-dns module can be held back by something it depends on — caddy-dns/vercel is, today — and that surfaces as a Go build error minutes in. DNS_MODULE_REPLACE in .env is the way past it, and the CLI fills it in for the cases it knows about:

DNS_MODULE_REPLACE=github.com/libdns/vercel=github.com/libdns/[email protected]

Delete that line once the module's maintainer tags a release.

Route 53, Azure, Google Cloud, Namecheap, Porkbun, OVH and about forty others need several values and cannot be expressed by the Caddyfile's one-line form. Choose them anyway, so the right module is built in, and write the provider block by hand in the Caddyfile; the CLI warns when you pick one. firetower upgrade rewrites firetower.yml and never touches the Caddyfile, so the edit survives.

Bringing your own certificate

For a corporate CA, a provider with no Caddy module, or a machine that cannot reach a Go module proxy:

firetower install --domain firetower.example.com

--domain without --dns-provider means exactly what it always did — a certificate you supply. Put fullchain.pem and privkey.pem in certs/, covering both the name and *.the-name, and uncomment the tls line in the Caddyfile. Nothing renews it for you, and firetower doctor warns when there are under three weeks left.

Behind a reverse proxy you already run

Not supported yet. Firetower serves preview hostnames itself, on the Host header, and routing those through a proxy you already run does not work — the deployment went on minting *.localhost previews behind it, which is an interface that works and previews that do not.

It is not an answer the CLI offers any more, and --public-url is gone with it. A deployment that already has this shape keeps upgrading.

If you want it: https://github.com/firetower-cloud/firetower/issues

Changing your mind later

install makes a deployment; it does not edit one. To move an existing one — onto a mesh address after opening it up, onto a new name, or onto a rotated token — use firetower domain:

firetower domain firetower.example.com          # asks the same questions install does
firetower domain firetower.example.com --dns-provider cloudflare --dns-token "$TOKEN"
firetower domain --https-bind 100.69.206.104    # same name, different address

It changes nothing about the release: no images are pulled, no migrations run, no database is touched. It recomputes the values in .env that follow from the answer, shows you the diff — with the API token masked — and recreates the containers that have to read them.

It is also how a deployment installed on loopback gets a name, which is the one way out of a shape that no longer installs.

Older releases

Choosing the ports needs a Firetower release that reads HTTP_PORT, and holding the control plane to loopback behind Caddy needs one that reads HTTP_BIND. Against an older one the CLI says so rather than writing a value nothing honours — and in the second case it says plainly that the release publishes on every interface, rather than promising a privacy it cannot deliver.

Requirements

Docker, the Compose plugin, and Node 20 or newer on the machine you are installing onto. Node comes with npm, which you needed to install this.

Commands

firetower install              install the control plane on this machine
firetower domain [name]        change the name or the address it is reached on
firetower upgrade              upgrade it, then report which workers lag
firetower status               version, health, hosts, worker drift
firetower doctor               diagnose a deployment that isn't working
firetower logs [service] [-f]  tail it
firetower start | stop | restart
firetower backup [--out DIR]   pg_dump plus the root key
firetower uninstall            tear it down, asking separately about volumes

firetower --version            this CLI's version, and the deployed one

Global flags: --dir <path> (remembered after install), --yes for unattended runs, --json on any command that answers a question.

install flags: --domain, --dns-provider, --dns-token, --https-bind, --advertise, --http-port, --https-port, --admin-username, --acme-email.

--https-bind is the address Caddy listens on. --advertise is the address people reach it on, and is needed only where the machine cannot bind that one — behind NAT, a floating IP or a load balancer. Given neither, an unattended install takes the single mesh address, or stops and names the flag when there is no mesh address or more than one. It never guesses between several.

Workers are not installed by this CLI. A worker is one binary on the machine that runs agents, put there by Firetower itself from Compute → Add a machine, or by hand with curl -fsSL https://usefiretower.com/worker.sh | sh. firetower worker … used to install a worker container here; those commands are gone with the container.

firetower tunnel is gone. It forwarded a control plane published on loopback, and that shape no longer installs; running it now says so rather than answering unknown command, for a release or two.

Unattended

firetower --yes install --domain firetower.example.com \
  --dns-provider cloudflare --dns-token "$TOKEN" --admin-username admin

--domain is required: a --yes with no domain used to mean the loopback shape, and now stops rather than quietly installing something else.

Generates the administrator password and writes the root key to firetower-root-key.txt in the deployment directory, because there is nobody there to read it off the terminal. Move it somewhere safe and delete it.

Where the deployment files come from

install fetches deploy/firetower.yml and deploy/Caddyfile from the latest release of firetower-cloud/firetower, so the compose file always matches the images being pulled. Copies under fallback/ are used only when GitHub is unreachable, and the CLI says so when it uses them.

This is why they are not vendored: a copy that were authoritative would go quietly out of step every time the main repository changed one.

The rule this codebase is built around

A value already in .env is never replaced.

FIRETOWER_ROOT_KEY is why. Every credential Firetower holds is sealed with it, so writing a new one over an existing database does not fail — it succeeds, and every stored credential becomes undecryptable, and nothing says so until the next clone. POSTGRES_PASSWORD is the same mistake with a louder symptom: it is baked into the data directory at initdb.

src/env.ts reads first and fills only what is absent. There are unit tests for it and an end-to-end test that installs twice and asserts the key survived.

One value an upgrade adds rather than carries, and it is the only one: FIRETOWER_UPDATER_TOKEN. It is what the control plane and the updater beside it recognise each other by, and a deployment installed before the updater shipped has no line for it — so the updater refuses every request and the Updates screen can upgrade the workers but not the machine it is running on. Nothing said so, because the compose file spells the variable ${FIRETOWER_UPDATER_TOKEN:-} and Compose starts without complaint.

So upgrade generates one when the key is absent or empty, shows it in the plan block as unset → ••••••••, and never touches a value that is already there. It is the one secret that is safe to invent: nothing is sealed with it and no data directory baked it in, so unlike the two above a fresh value costs nothing once both containers are recreated. firetower doctor reports a deployment that is still missing it.

Staying current

install and upgrade ask two questions before they touch anything: whether npm has a newer CLI, and whether the current Firetower release requires one.

The second is the one with teeth. A release that changes what a deployment needs declares it in deploy/cli.json in the main repository:

{ "minimumCli": "0.5.0", "reason": "the compose file now needs FIRETOWER_X" }

A CLI below that minimum refuses to go on and offers to upgrade itself, because an old CLI does not fail cleanly — it writes a .env missing a variable Compose now requires, or waits on a service that has been renamed, and the error the operator reads is about neither. A newer version merely existing on npm is a note, not a block.

The file is absent today and that is a supported answer: no requirement. Being unable to reach npm or GitHub is also not a block — offline is not a reason to refuse to work. --skip-version-check opts out entirely.

Versioning

Independent semver, starting at 0.1.0. It tracks this CLI, not Firetower — the two were coupled only while the CLI pinned image tags, and it uses :latest.

release-please reads the commit messages on main and keeps one open pull request — chore(main): release 0.2.0 — carrying the version bump and the changelog entry. Nothing publishes until that pull request is merged; merging it tags, and the tag triggers npm publish. So cutting a release stays a review rather than a side effect of merging a feature.

Below 1.0, bump-minor-pre-major keeps a breaking change on the minor.

Commits

Conventional commits, enforced in CI. This is not a style rule: the messages are the input to versioning, and one that says neither feat: nor fix: produces a release that bumps nothing and explains nothing.

feat(install): preflight the machine before writing anything
fix(upgrade): read the database name from the compose file

Scopes: install, upgrade, worker, doctor, status, backup, env, ci, deps. Headers stay under 72 characters.

CI checks the pull request title as well as the commits, and the title matters more — a squash merge throws the commits away and keeps the title, which is then the only thing release-please ever sees.

To catch it before pushing:

echo "feat(install): …" | pnpm commitlint

Releasing

One secret, once: NPM_TOKEN — a granular npm automation token scoped to publish @firetower/cli and nothing else. Settings → Secrets and variables → Actions.

Nothing else is needed. release-please uses the built-in GITHUB_TOKEN, and the workflow already declares the permissions it wants. One repository setting does have to be on, though, or release-please fails opening its pull request with an error that does not say so: Settings → Actions → General → Allow GitHub Actions to create and approve pull requests.

Once 0.1.0 is on the registry the token can go away — npm trusted publishing signs over OIDC with no stored credential, and the publish job already requests the id-token: write it needs. It cannot be set up before then: a trusted publisher is configured on the package's own settings page, and that page does not exist until the package does.

Development

pnpm install
pnpm build
pnpm test          # fast, no Docker
pnpm test:e2e      # drives a real daemon; minutes

Licence

AGPL-3.0-only, the same as Firetower.