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

@arsenalc2/cli

v0.9.0

Published

Publish ATAK plugins to an Arsenal C2 registry. Metadata is read off the APK, so a push is the artifact and a channel.

Readme

@arsenalc2/cli

arsenal publishes ATAK plugins to an Arsenal C2 registry.

You describe nothing. The registry reads the package name, versionCode, versionName, target ATAK version, CIV/MIL flavor, native ABIs and the signing certificate off the APK itself, so a push is the artifact plus a channel. There is no metadata form to fill in and nothing that can drift from the thing you actually built.

npm i -g @arsenalc2/cli

Node 20 or newer. No runtime dependencies.

Authenticate

Create a token on your profile page, then put it in the environment:

export ARSENAL_TOKEN=arc2_xxx

That is the only configuration. ARSENAL_URL overrides the registry host if you run your own; leave it unset for the public one.

Publish a build

arsenal push app/build/outputs/apk/civ/release/plugin.apk \
  --channel beta \
  --release-name 1.4.0 \
  --notes "Fix range ring snap"

Channels are alpha, beta, rc and release. Every new release needs a semver release name (below).

Each push is a new release. A channel's latest release, what devices on it are offered, is the newest release on it that is not yanked, as on Google Play (below).

A release that ships more than one build - CIV and MIL, two ATAK targets, an ABI split - is one push per build. A build joins an existing release when it has the same versionCode and the same versionName apart from the ATAK template's - [5.6.0] suffix, so a matrix is a loop in your CI job and needs no flag:

for apk in build/outputs/**/*.apk; do arsenal push "$apk" --channel beta --release-name 1.4.0; done

If your builds' names or versionCodes differ per variant (a versionCode that folds in the ATAK target, say), name the release they join: arsenal push <apk> --release 10204. Pushing the same build twice is what is refused.

A release is addressed by its versionCode, or <versionCode>-<n> when an earlier release already carried that code. A tak.gov build zipped without .git reports versionCode 1 on every release, and that works: each one is its own release (1, 1-2, 1-3).

Which release is latest

Nothing is pointed by hand (#349): a channel's latest release is computed. It is the newest release on the channel by semver precedence of the release name (releases published before names were required sort below every named one, by versionCode) that is not yanked and still has a build that is not. A device that cannot run it is offered the newest release below it that it can.

  • Push a newer release and it is the latest from the next catalog read.
  • Push an older one, a vendor backfilling a catalog or a 1.2.1 fix after 1.3.0, and it is stored, listed and installable from history below the latest release, which stays.
  • Stage a release by pushing it to a lower channel and promoting it when it is ready.
  • Roll a channel back by yanking its latest release, arsenal yank <package> <release>: the channel falls back to the newest release below it, and --undo restores it.

arsenal make-current and --no-make-current are gone; the registry refuses the makeCurrent form field an older CLI sends, so a script fails rather than publishing.

Promoting a release

promote moves a release up to a higher channel without uploading it again, where it becomes the latest release, the way Google Play promotes a release from one track to the next:

arsenal promote com.example.plugin 42 --channel release
# com.example.plugin: latest on release is now release 42 (1.3.0), was 41 (1.2.0); 42 is on beta, release

The build that ships is the exact file that was tested: same address, versionCode, digest and name. It stays on every channel it was on, so testers granted only beta keep it. Promotion goes up only (alpha, beta, rc, release), skipping a level is fine, and it is refused (promotion_not_newest) when the release is not newer than the latest on the target: to go back, yank the latest release instead. Onto rc or beta needs what a push needs; onto release needs an admin or owner and a "full" token. Running it again once done is not an error. It warns, as a push does, when no build of the release loads on official ATAK, when a build's versionCode is below what the target channel already served, and when a pre-release name goes onto release. A MIL build is refused onto a channel granted to everyone.

Two ways of naming pre-releases both work, and which one is yours to choose:

  • Rebuild per stage: 1.3.0-beta.1, 1.3.0-beta.2 on beta, then a new build named 1.3.0 pushed to release. Nothing is promoted.
  • Build once and promote: name builds for the version they will become (1.3.0), push them to beta, and promote the one that holds up. Promotion never renames, so a build named 1.3.0-beta.2 ships on release under that name.

A release none of whose builds loads on official ATAK (unsigned, or signed by a key official ATAK refuses) becoming the latest on release is allowed, and both the push and promote print a wont_load_official warning saying what devices on official ATAK will see, and which yank falls back to a release that loads there.

Naming the release

Every new release needs a semver release name (1.4.0, 1.4.0-beta.2, 1.4.0+ci.7), on every plugin. It is what the release is listed under and what the registry orders releases by: semver precedence, so 2.5.0-rc.9 is older than 2.5.0. A push that creates a release without one is refused with release_name_required, and one with a name that is not semver with release_name_not_semver. Give it one of three ways:

  • push from a semver tag. With no flag, the CLI reads GITHUB_REF_NAME when GITHUB_REF_TYPE=tag (GitHub Actions) or CI_COMMIT_TAG (GitLab), strips one leading v (v1.4.0 becomes 1.4.0), and uses it if the rest is semver. Never a branch.
  • set ARSENAL_RELEASE_NAME in the job, which beats a tag.
  • pass --release-name, which beats both.

The CLI prints which one it used: release name 1.4.0 (from GITHUB_REF_NAME). A CLI from before this rule cannot read the name from CI, so upgrade it: npm i -g @arsenalc2/cli@latest.

for apk in build/outputs/**/*.apk; do
  arsenal push "$apk" --channel release --release-name 2.4.0
done

Write prerelease counters with a dot: beta.10, not beta10. Semver compares beta10 as text, so it sorts below beta9; the registry says so when it sees one.

The name is the release. A push carrying a name joins the release already declared under exactly that name, whatever its versionCode (so a 5.4.0 build at versionCode 1 and a 5.6.0 build at a commit timestamp are one release when both say 1.1.42), and otherwise creates that release. A push with no name can only add a build to an existing release: the one whose versionCode and versionName (apart from - [5.6.0]) it matches; when it matches several that lack this build, it is refused (release_ambiguous) until you name the release. With --release, the named release is joined, and a --release-name that disagrees with its name is refused; a name inferred from CI is not sent with --release, so a tag pipeline can still add a build to a release published before names were required.

Releases published before names were required have no name, and every release published before this rule is one of them. A named push of one of their versions (pushing 1.3.0 when an unnamed release already is 1.3.0) is refused with release_name_matches_legacy, because it would otherwise create a second 1.3.0 and make it the latest. Add the build to the existing release with --release <ref>, which the refusal names, and no --release-name. Each build keeps the versionName it carries, and the registry warns when that names a different version than the release (only the MAJOR.MINOR.PATCH is compared, so a 2.5.0-beta.1 release shipping an APK that says 2.5.0 is fine).

A release name must be unique within the plugin, and so must its precedence: 2.5.0+a and 2.5.0+b are one version. Either is refused with release_name_taken before anything is uploaded. Yanked releases keep their names.

The registry lints every build as it lands, and reports what it found. The ones worth knowing about describe failures that are otherwise silent on a tester's device:

  • versionCode reused: this release carries the same versionCode as another. Android still installs it as an update, but an MDM keyed on versionCode will not see a new build. Include .git in your tak.gov submission zip, or set versionCode in build.gradle yourself.
  • will not load on official ATAK builds, because the APK carries a non-TPC signer. A warning: it is correct for a build meant for sideloading onto dev devices.

You find out here rather than from a device that quietly never shows the plugin.

A plugin keeps the name it was first published under

The plugin's listed name is its android:label, read off the artifact like everything else, and the first published build sets it. A later push whose label differs is refused, on every channel:

display_name_changed
  {"published":"Plugin Template","pushed":"ATAK Reactive Example"}

The reason is the devices. The package name has not changed, so everyone who installed the old build is offered this one as a routine update, under a name they never chose - and a publisher correcting a template leftover looks identical, from the registry's side, to one quietly becoming "TAK Core". Worse, the name ATAK shows in its own plugin menu is read off the APK rather than from us, so there is nothing we could annotate to explain it after the fact. Declining to distribute the build is the only thing that actually protects the device.

Restore android:label to the published name and push again.

Case and whitespace are not a rename: ATAK Reactive example to ATAK Reactive Example, or a stray trailing space, goes through and updates the stored name.

There is deliberately no flag to override this. If you need a genuine rename - a rebrand, a template leftover, a transferred namespace - it wants a deliberate operation that also requires your next build's label to match the new name, so that the listing and the device can never disagree. That is not built yet; ask, rather than working around it by republishing under a new package name, which would orphan every install.

Other commands

arsenal promote com.example.plugin 42 --channel release        # move a release up a channel
arsenal yank com.example.plugin 42                              # withdraw a release; --undo restores it
arsenal delete com.example.plugin 42                            # remove a release, every build of it
arsenal delete com.example.plugin 42 --artifact atak-apk-civ-5.6.0-arm64
arsenal grant com.example.plugin --channel beta --team testers
arsenal revoke com.example.plugin --channel beta --team testers
arsenal grants com.example.plugin                               # who holds which channel
arsenal --help

Prefer yank to delete: it keeps the record and can be undone. delete is for a release that was never one, such as a pull request build replaced by the next push (--notes-prefix), or a build that leaked a secret. Yanking, deleting and the grant commands need an admin or owner and a "full" token. yank, grant and revoke are safe to repeat, so a CI job can run them unconditionally. revoke --user takes any verified address of the person's account, the same as grant.

Reading the registry

These print to stdout, and take --json. A "read" token is enough:

arsenal list                                                    # the namespace's plugins
arsenal versions com.example.plugin --channel beta             # releases and their builds
arsenal download com.example.plugin 42 --output plugin.apk     # fetch a build, sha256 verified

download checks the bytes against the sha256 the registry holds and removes the file if they differ. A release with several builds needs --artifact <id>, and download lists the ids when it is missing. Downloading needs the owner, admin or publisher role: anyone else installs through the Arsenal C2 client.

In CI

The token is the whole setup. A restricted token infers its organization, so --org is usually unnecessary:

# On a tag push (v1.4.0): the release is named 1.4.0 from the tag, no flag needed.
- run: npm i -g @arsenalc2/cli@latest
- run: arsenal push build/plugin.apk --channel release --notes "$GITHUB_REF_NAME"
  env:
    ARSENAL_TOKEN: ${{ secrets.ARSENAL_TOKEN }}

Not building from a tag? Set ARSENAL_RELEASE_NAME (for example 1.4.0-beta.${{ github.run_number }}) or pass --release-name.

Full documentation is at arsenalc2.com/docs.

License

Apache-2.0