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

@asteriusit/ctcpy

v2.0.0

Published

Copy container images between registries (skopeo-copy-like) in pure Node.js, zero runtime dependencies. Supports Nexus path-based repositories.

Readme

ctcpy

Copy a container image from one registry to another — like skopeo copy — in pure Node.js (>= 18), with zero runtime dependencies. No Docker daemon, no Go binary: just the Docker Registry v2 / OCI HTTP API.

Designed for environments where only npm packages can be imported (e.g. through a Nexus npm proxy) and Go-based tooling is blocked.

What it does

  • Copies tag or digest references between any v2-compatible registries (Nexus, Harbor, GitLab, ECR, Docker Hub with explicit host…).
  • Handles multi-arch images (manifest lists / OCI indexes): all platform manifests and their blobs are copied, digests preserved byte-for-byte.
  • Streams blobs (constant memory, works with multi-GB layers).
  • Skips blobs already present in the destination (idempotent re-runs, cheap syncs).
  • Auth: Basic auth per side, plus the Bearer token challenge flow if the registry requires it.
  • Addresses registries that don't serve /v2/ at the root — notably Nexus path-based repositories (/repository/<name>/v2/…) via --src-prefix / --dest-prefix.

Install

Published as the scoped package @asteriusit/ctcpy. The installed command is ctcpy.

npm install -g @asteriusit/ctcpy
# or run without installing:
npx @asteriusit/ctcpy <src> <dest>

Usage

export CTCPY_SRC_CREDS="user:password"     # source registry credentials (optional)
export CTCPY_DEST_CREDS="user:password"    # destination registry credentials (optional)

ctcpy nexus-dev.corp:8443/team/myapp:1.2.3 nexus-prod.corp:8443/team/myapp:1.2.3

Reference format: registry-host[:port]/repo/path[:tag|@sha256:…] (tag defaults to latest). The destination must be a tag. Use --plain-http-src / --plain-http-dest for registries served over plain HTTP.

Credentials are read from environment variables only, so they never end up in shell history or ps output.

Options

| Option | Meaning | | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | --plain-http-src, --plain-http-dest | Use http instead of https for that side. | | --src-prefix <path>, --dest-prefix <path> | Base path inserted before /v2/ (e.g. /repository/docker.io). Also settable via CTCPY_SRC_PREFIX / CTCPY_DEST_PREFIX. | | -h, --help | Show usage. |

Nexus path-based repositories

Nexus exposes each docker repository under /repository/<name>/v2/… rather than at the registry root. Point ctcpy at the Nexus host and name the repo with a prefix. To copy an image from a docker.io proxy repo into a docker-prod hosted repo on the same Nexus:

export CTCPY_SRC_CREDS="user:password"
export CTCPY_DEST_CREDS="user:password"

ctcpy \
  localhost:8081/library/hello-world:latest \
  localhost:8081/library/hello-world:latest \
  --plain-http-src --plain-http-dest \
  --src-prefix  /repository/docker.io \
  --dest-prefix /repository/docker-prod

Alternatively, configure a Docker HTTP connector port on each Nexus repo (Repository → the repo → HTTP connector). That serves the repo at the root /v2/, and ctcpy (like docker and skopeo) can then target host:port/... with no prefix.

Use as a library

import { RegistryClient, copyImage, parseImageRef } from '@asteriusit/ctcpy';

const src = parseImageRef('localhost:8081/library/hello-world:latest');
const dst = parseImageRef('localhost:8081/library/hello-world:latest');

await copyImage(
  {
    repository: src.repository,
    reference: src.reference,
    client: new RegistryClient({
      host: src.host,
      plainHttp: true,
      pathPrefix: '/repository/docker.io',
      credentials: 'user:password',
    }),
  },
  {
    repository: dst.repository,
    reference: dst.reference,
    client: new RegistryClient({
      host: dst.host,
      plainHttp: true,
      pathPrefix: '/repository/docker-prod',
      credentials: 'user:password',
    }),
  },
);

Make npx @asteriusit/ctcpy work in your organization

Publish this package to your internal Nexus npm hosted repository. Scoped packages need --access when published to public registries; for an internal Nexus repo the registry flag is what matters:

npm publish --registry https://nexus.corp/repository/npm-internal/

Point the @asteriusit scope at that repository so it resolves without a global registry change:

npm config set @asteriusit:registry https://nexus.corp/repository/npm-group/

Then anyone whose npm is configured against your Nexus npm group can run npx @asteriusit/ctcpy <src> <dest>.

Development

npm install
npm run build        # compile TypeScript (src/ -> dist/)
npm run typecheck    # tsc --noEmit
npm run lint         # eslint (type-aware)
npm run format       # prettier --write
npm test             # build, then run the test suite

The test suite spins up in-memory mock registries (root-served and Nexus-style path-prefixed), seeds a multi-arch image, runs the built CLI, and asserts byte-identical manifests/blobs plus idempotent re-runs.

Releasing

Publishing uses npm trusted publishing (OIDC) — no NPM_TOKEN secret. Pushing a semver tag triggers .github/workflows/release.yml, which builds, tests, and publishes with automatic provenance.

One-time bootstrap (required once per package)

npm's OIDC trusted publishing cannot perform a package's first publish — the package must already exist before a trusted publisher can be attached (an anti-name-hijacking safeguard; see npm/cli#8544). This bootstrap needs no CI token; a tokenless interactive login is enough.

  1. Create the package once, tokenless, from a machine (browser-based login with 2FA — not an automation token):

    npm login                              # opens the browser, uses your 2FA
    npm publish --access public            # creates @asteriusit/ctcpy on npm

    This bootstrap publish carries no provenance — that's expected. Provenance is generated automatically for every subsequent CI release via trusted publishing, so provenance is intentionally not forced in publishConfig (forcing it would break this tokenless publish, which has no CI OIDC provider).

    Prefer a throwaway placeholder so CI still owns every real version? Publish 0.0.0 first, then let the tag workflow publish the real one:

    npm version 0.0.0 --no-git-tag-version && npm publish --access public
    npm deprecate @asteriusit/[email protected] "placeholder for trusted-publishing bootstrap"
    git checkout package.json              # restore the real version
  2. Attach the trusted publisher on npmjs.com → the package → Settings → Trusted Publisher → GitHub Actions:

    | Field | Value | | ----------------- | ------------- | | Organization/user | asteriusit | | Repository | ctcpy | | Workflow filename | release.yml | | Allowed action | npm publish |

Every release after that

npm version patch      # bumps package.json and creates a matching tag
git push --follow-tags

The workflow verifies the pushed tag matches package.json's version, then publishes via OIDC with no secrets. Requirements (already set in the workflow): a GitHub-hosted runner, id-token: write, and npm ≥ 11.5.1.

Project layout

src/
  cli.ts        argument parsing and the executable entry point
  copy.ts       single- and multi-arch copy orchestration
  registry.ts   Docker Registry v2 client (auth, manifests, blobs, prefix)
  reference.ts  image-reference and prefix parsing
  types.ts      shared data-model types
  errors.ts     UsageError / RegistryError
  logger.ts     stderr progress + size/digest formatting
  index.ts      public library API

Limitations (by design, keep it simple)

  • No docker-daemon: / oci-archive: transports — registry-to-registry only.
  • Foreign/urlless layers (foreign.diff.tar.gzip with urls) are not fetched from external URLs.
  • No signature copying.