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

@git.zone/cli

v9.2.2

Published

A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.

Readme

@git.zone/cli 🚀

@git.zone/cli is the development workflow CLI behind the gitzone and gzone commands. It helps TypeScript-heavy teams keep projects tidy, create semantic source commits, manage local Docker-backed services, scaffold new modules, and release software through explicit, target-based release configuration.

It is opinionated where that saves time: source commits and releases are separate, changelog entries flow through a standard Pending section, project config lives in .smartconfig.json, and release targets make side effects visible before they happen.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm add -g @git.zone/cli

After installation, both binaries point to the same CLI:

gitzone --help
gzone --help

The Big Idea

gitzone commit handles source history.

gitzone release handles release transactions.

That split is intentional. A commit should not unexpectedly publish npm packages, push Docker images, or trigger remote release pipelines. A release should clearly show which targets it will publish to.

Quick Start

# Preview project standardization work
gitzone format

# Apply formatting changes
gitzone format --write

# Create a semantic source commit
gitzone commit

# Preview the configured release transaction
gitzone release --plan

# Release pending changelog entries to configured targets
gitzone release

Commands

| Command | Purpose | | ----------- | --------------------------------------------------------------------------- | | commit | Analyze changes and create one semantic source commit | | release | Turn pending changelog entries into a versioned release and publish targets | | format | Plan or apply project formatting and standardization | | config | Inspect, update, and migrate .smartconfig.json | | services | Manage local MongoDB, ObjectStorage, and Elasticsearch containers | | tools | Manage the global @git.zone toolchain | | template | Scaffold projects from built-in templates | | meta | Manage multi-repository workspaces | | open | Open repository assets like CI pages | | docker | Report and reclaim Docker resources created by git.zone tooling | | deprecate | Deprecate npm packages across registries | | start | Prepare an existing project for local work | | helpers | Run small helper utilities |

Global flags include --help, --json, --plain, --agent, --no-interactive, and --no-check-updates.

Toolchain Management

gitzone tools replaces the former gtools command from @git.zone/tools. It manages globally installed @git.zone development tools through pnpm.

# Check installed @git.zone tools and update outdated packages
gitzone tools update

# Update without prompts
gitzone tools update -y

# Install missing managed @git.zone tools
gitzone tools install

gitzone tools update checks @git.zone/cli first. If the CLI itself needs an update, it updates @git.zone/cli and asks you to rerun the command before updating the rest of the toolchain.

Commit Workflow

gitzone commit creates one semantic source commit. It does not bump versions, create tags, publish packages, or push Docker images.

# Interactive semantic commit
gitzone commit

# Read-only AI recommendation
gitzone commit recommend --json

# Auto-accept safe recommendations
gitzone commit -y

# Auto-accept a breaking recommendation explicitly
gitzone commit -y --allow-breaking

# Auto-accept, test, build, and push
gitzone commit -ytbp

# Show the resolved workflow without mutating anything
gitzone commit --plan

# Supply a message and changelog entry without AI
gitzone commit -y --message 'fix(cache): expire stale entries' --changelog 'Expired entries are removed before lookup.'

# Preserve a multiline message and custom Pending Markdown
gitzone commit -y --message-file /tmp/commit.txt --changelog-file /tmp/pending.md

The commit flow:

  1. Analyze the working tree.
  2. Suggest commit type, scope, and message.
  3. Write a human-readable entry into changelog.md under ## Pending.
  4. Stage and create one semantic source commit.
  5. Optionally run formatting, tests, build, and push based on flags or config.

Commit flags:

| Flag | Meaning | | ------------------ | -------------------------------------------------------------- | | -y, --yes | Auto-accept safe recommendations | | --allow-breaking | Allow -y/--yes to accept BREAKING CHANGE recommendations | | -t, --test | Add test step | | -b, --build | Add build step | | -p, --push | Push after the source commit | | -f, --format | Run gitzone format --write before commit | | --plan | Show resolved workflow only | | -m, --message <text> | Use a complete semantic commit message without AI | | --message-file <path> | Read the complete message from a UTF-8 file | | --changelog <text> | Supply custom Pending text with a manual message | | --changelog-file <path> | Read custom Pending text from a UTF-8 file |

Manual messages start with type(scope): description; the scope is optional. Conventional ! subjects and BREAKING CHANGE: footers are supported. The entire message, including its body, is passed literally to Git. File inputs preserve multiline text; CRLF becomes LF and terminal newlines are normalized. Each input is limited to 128 KiB. Use --message or --message-file once, and at most one changelog option. Changelog overrides require a manual message.

Without a changelog override, GitZone derives the Pending entry from the semantic subject and body. Custom text becomes an entry in the message's semantic bucket. For complete Markdown, supply a Pending fragment such as:

### Fixes

- Remove expired entries before lookup.
  - Preserve entries whose validity has not expired.

Fragments may contain ### Breaking Changes, ### Features, ### Fixes, ### Documentation, and ### Maintenance. Omit the document title, ## Pending, and version headings. GitZone merges entries into the existing Pending buckets and preserves previous entries and release history. Pending headings continue to determine the release bump; a breaking message requires a Breaking Changes entry, and either a breaking message or new breaking changelog text requires the usual interactive confirmation or -y --allow-breaking.

Manual mode replaces the AI analysis step with manual. It keeps the configured format, test, build, staging and push behavior. --plan validates and displays the message and changelog without mutation or AI access. A clean repository remains unchanged. Input is validated before workflow steps run; it never falls back to AI after an input error. Place input files outside the repository if they should not be included by the existing stage-all workflow.

-r is intentionally not part of commit anymore. Use gitzone release.

Release Workflow

gitzone release releases from main. Running it on another branch fails before release metadata or ref mutation unless the bare --merge flag is explicitly supplied, or the branch carries a release.line configuration that names it (see Maintenance lines).

--merge is intentionally strict: the source and existing main worktree must be clean, the source must be linearly rebased onto current remote main, and the Git target must push both the branch and tag. GitZone pins the single configured push URL, compare-and-swap fast-forwards local main, updates its verified worktree, and lease-pushes that exact source commit before creating release metadata. It rejects diverged or merged history, local replacement refs or grafts, stale worktrees, incomplete shallow history, changed destinations, and remote races. If the pre-release push fails, GitZone restores local main only when doing so cannot overwrite concurrent work. This plumbing-level integration intentionally does not run merge hooks or write ORIG_HEAD.

gitzone release performs the release core once, then publishes to configured targets. When Docker is active, its immutable candidate must qualify before any Git, npm, or OCI destination publication. An active Git target must then succeed and be verified before npm or OCI publication can start.

Before interactive confirmation or source mutation, a Docker release verifies the project-local tSDocker capabilities and validates the deterministic request against the source commit. After the release commit and configured build, GitZone validates the final request against the release commit immediately before atomically installing the schema-2 journal.

The release core is not configurable plumbing. It always follows the same professional release transaction:

  1. Verify the release branch, clean state, and worktree ownership of main (or of the maintenance branch a release line names). A project that configures packageFiles must carry exactly the files list it generates (step preflight.packageFiles, see Package contents). A release that builds or packs a commit with a package.json also proves here that the source commit carries a pnpm-lock.yaml its checkout can install with --frozen-lockfile. An npm release then reads every registry's dist-tags for every package it publishes: latest never moves downwards, and a line takes latest only while every higher major is a prerelease (step preflight.npmDistTags, see A line that keeps latest).
  2. Read changelog.md ## Pending entries and infer or accept a semver bump, raised to release.versionFloor when that is higher.
  3. Run configured tests.
  4. With --merge, fast-forward and lease-push main before release metadata is created.
  5. Update version files and baked commit info.
  6. Move pending changelog entries into the new version section.
  7. Create the local release commit and tag on main, and record the release intent, so any later failure is resumable (see Resuming a release that failed before its journal).
  8. Create a disposable detached checkout of that release commit, install its locked dependencies when the commit carries a package.json, and run the configured release build there. An active Docker target additionally builds the release worktree its qualification reads.
  9. When npm is selected, prepare every named tspublish module outside the repository, then pack each module and the public root package once into an exact tarball from that checkout. Every tarball must then carry each entry point its packed package.json names (core.packNpmArtifact.entryPoints), and its estimated publish body must fit each registry's maxPublishBytes (core.packNpmArtifact.publishSize, planned only when a limit is configured); the packed size and the body per registry are printed either way. See Package contents.
  10. Atomically install a durable journal before final publication. A release that publishes only the root package to npm uses journal schema 1, one that also or only publishes tspublish modules uses schema 3, a Docker release uses schema 2 with or without modules, and a Gitea asset release uses schema 4, all in the same storage tree.
  11. For schema 2, run the project-local tSDocker qualification build and configured image tests, then durably record the candidate digest graph and complete ordered promotion set.
  12. Publish and verify Git, then every npm package at every registry, modules first and the root package last, then each Docker destination. Remove only the exact qualified candidate after every destination is verified, or after a terminal qualification destination conflict before Git/npm publication.
  13. Remove the test artifacts tstest wrote, so a machine does not accumulate test output release after release. This runs only after a successful publication, leaving the logs in place when a release fails. It is skipped when the project has no tstest installed, and a project opts out with {"@git.zone/tstest": {"cleanup": {"onRelease": false}}} in .smartconfig.json. See the tstest readme for what it removes; snapshots are kept.

The release builds and packs the release commit

A release publishes what its release commit contains. After the commit and tag exist, GitZone creates a disposable detached worktree of that exact commit under the system temporary directory, runs pnpm install --frozen-lockfile in it when that commit carries a package.json, and runs the configured release build and pnpm pack there. Gitea assets are frozen from the same checkout. The checkout must still be the exact commit with a clean tree before and after packing, and it is removed on success and on failure. Recovery (gitzone release recover) has always worked this way; every release now does.

Consequences worth knowing before upgrading:

  • The release build no longer refreshes dist_* in your working folder for npm-only, Git-only, and Gitea-asset releases. Run pnpm build yourself when you want fresh local output.
  • A build that depends on untracked or ignored local files now fails instead of publishing output nobody else can reproduce.
  • Untracked files that match the files globs can no longer reach the tarball.
  • Unreadable paths anywhere in the working folder, such as container-owned service data under .nogit/, no longer break packing. pnpm walks the whole project tree before applying files, and the checkout does not contain that data.
  • A release commit that carries a package.json must install from its own committed lockfile. Preflight proves that before anything is created: the source commit has to carry a tracked pnpm-lock.yaml, and pnpm install --frozen-lockfile --lockfile-only — pnpm's no-install, no-write form of the same check — has to accept it. A lockfile that is missing, ignored, or outdated stops the release there, with no release commit, no tag, and nothing pushed. Commit the lockfile, or run pnpm install and commit it, then release again. The version bump only changes version, which never changes a dependency specifier, so a lockfile accepted in preflight is still accepted in the release commit.
  • Pnpm commands use the scope carried by the release commit. A repository that tracks its own root pnpm-workspace.yaml retains that workspace's settings and member validation. Otherwise, lockfile verification and checkout installation use --ignore-workspace. For a committed root workspace, configured test and build commands clear inherited workspace overrides and let each pnpm process discover the workspace from its own working directory. Independent fixture projects can therefore install their own dependencies, including their own workspace members. Without a committed root workspace, configured commands retain the repository directory as a forced pnpm scope; independent fixture installs are not supported in that mode. Both modes exclude an unrelated enclosing workspace from release lockfile validation and installation.
  • The release commit must carry a package.json: it holds the version gitzone releases. A commit without one is refused before anything is created.
  • The checkout holds only committed files, so a gitignored project-level .npmrc is not in it. A project that authenticates private dependencies through such a file now fails its checkout install after the local commit and tag exist. Authenticate at user level instead: pnpm npm login --registry=<registry> — what gitzone config doctor suggests as well — writes ~/.npmrc, and the checkout reads that.
  • An install that fails in the checkout for an environmental reason — an unreachable registry, a missing credential, an unusable store — stops the release with the local release commit and tag created, nothing pushed, and no release journal written. The release recorded its intent, so fix the environment and run gitzone release resume <version> -y: it prepares and publishes that exact tagged commit without moving any ref. A tag created by GitZone 7.2.6 or earlier has no recorded intent; a release with a Git target plus an npm and/or Docker target continues with gitzone release recover <version>, and other configurations of that age delete the tag and drop the release commit (git tag -d v<version> and git reset --hard HEAD^ on main) and release again. Confirm first that HEAD is the release commit — git log -1 --format=%s prints v<version> — so that a commit made on top of it is never dropped.
  • The --plan step list names the isolated work: preflight.lockfile for a project with a package.json, then core.releaseCheckout, core.releaseCheckout.build, then core.packNpmArtifact and its checks core.packNpmArtifact.entryPoints and, with a configured limit, core.packNpmArtifact.publishSize.

An active docker target additionally runs the build in the release worktree, as before, and the plan shows that step as core.build. tSDocker binds qualification to the project directory: the journaled request carries that cwd and a project identity hash derived from it, so the image is still built from the working folder and untracked files there can still reach an image. Moving Docker qualification to the release commit belongs to @git.zone/tsdocker and is not part of this release.

Targets decide what happens after that:

| Target | What it does | | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | git | Atomically pushes the exact release commit to main (or to the maintenance branch of a release line) and the new tag, often triggering remote CI release builds | | npm | Publishes the same journaled tarball to every configured registry; verifies its bytes anonymously, or its integrity record with the registry's configured authentication for private packages | | docker | Qualifies immutable OCI digest graphs with project-local tSDocker, promotes each journaled destination under single-writer alias fencing, verifies exact digests, then cleans the candidate; a terminal qualification conflict permits cleanup only |

# Preview the resolved release plan
gitzone release --plan

# Release to configured targets
gitzone release

# From a clean feature branch already rebased onto current main
gitzone release --merge

# Release only to npm
gitzone release --target npm

# Remove npm and Docker from the resolved targets; Git remains only if configured
gitzone release --no-publish

# Override inferred semver level
gitzone release --minor

# Run the release in its own session and return once it has started
gitzone release -y --detach

Release flags:

| Flag | Meaning | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | -y, --yes | Run without interactive confirmation | | -t, --test | Enable preflight tests | | -b, --build | Enable the release build after local release metadata is created | | -p, --push | Explicitly select the git target; combine with other target flags as needed | | --target <csv> | Select git, giteaAssets, npm, and/or docker; npm cannot omit a configured enabled Gitea asset target | | --npm | Explicitly select the npm target; combine with other target flags as needed | | --docker | Explicitly select digest-qualified Docker publication; requires project-local @git.zone/tsdocker >= 3.5.1 with protocol v1 | | --no-publish | Remove Gitea assets, npm and Docker from the resolved target set without implicitly enabling Git | | --no-build | Disable the separate post-metadata project build; Docker qualification still performs its required image build | | --merge | Fast-forward and lease-push a cleanly rebased feature branch into main, then release from main; incompatible with Docker or Gitea assets | | --major, --minor, --patch | Override inferred semver level; release.versionFloor still applies | | --plan | Show the resolved workflow without fetching or mutating refs, files, the index, or worktrees | | inspect [version] | Read one or all durable release journals; add --json for machine-readable output | | resume <version> | Resume journaled Git, npm, qualification, promotion, or cleanup work; fresh-release overrides are rejected, and --json remains inspect-only | | --recover-attempt <id> | Recover the exact 32-character lowercase hexadecimal attempt.id shown by inspect --json, after independently proving its publisher stopped | | --detach | With -y, run a release or resume in its own session after the foreground checks; see Running a release detached |

Version floor

release.versionFloor sets a minimum for the next release version. Use it when a repository must join a common version line, for example when every serve.zone package moves to major 32:

{
  "@git.zone/cli": {
    "release": {
      "versionFloor": "32.0.0"
    }
  }
}

GitZone first infers the version as usual. It starts from the current package.json version and applies the level from the Pending changelog or from --major, --minor or --patch. The release version is then the higher of that inferred version and the floor. The floor is committed configuration, so the plain gitzone release -y honours it without a flag, and nobody has to hand-edit package.json to a version that was never published.

gitzone release --plan states the outcome:

| Plan line | Meaning | | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | version floor: 32.0.0 applied (inferred 3.6.0 -> release 32.0.0) | The floor is higher than the inferred version and becomes the release version | | version floor: 3.5.4 satisfied by inferred 3.6.0 | The inferred version already meets the floor; nothing changes | | version floor: 32.0.0 inert (current 32.0.0 already meets it); remove release.versionFloor | The project has reached the floor; delete the setting | | version notice: the floor raises the major version above inferred 3.6.0, and Pending ... | The floor raised the major version above the inferred version and Pending has no ### Breaking Changes section; the line is informational and stays until such a section exists |

A floor does not require a ### Breaking Changes Pending section. --major never required one, and the floor is the same kind of explicit maintainer decision, reviewed in a commit. Aligning a version line is often not an API break, so requiring the section would force a false changelog entry. A major release is also the safe direction for consumers, because ^3 ranges never resolve to 32.0.0. The plan notice only makes that jump visible and never blocks the release. It appears only when the floor itself raises the major version; a major that --major produces prints no notice.

When the floor decides the version, the release journal records that decision with the release identity:

"release": {
  "version": "32.0.0",
  "tag": "v32.0.0",
  "mainOid": "...",
  "tagOid": "...",
  "versionFloor": { "floor": "32.0.0", "inferredVersion": "3.6.0" }
}

gitzone release inspect and gitzone release resume print the same version floor: ... applied line. The record is part of the journal's immutable identity. resume always publishes the journaled version and never resolves a new one. This matters because after the release commit the floor is inert, and a fresh resolution would infer 32.0.1. recover takes its version from the existing tag and writes no floor record, because the failed run did not record its level override.

The floor must be a canonical x.y.z string whose components do not exceed 9007199254740991, except for a matching prerelease channel. Every rejection throws ReleaseVersionError, and its code names the rule that failed. malformed-version-floor and unsupported-prerelease-floor are configuration errors that --plan and gitzone config doctor report. --plan and the release raise noncanonical-version when the current package.json version is not canonical. current-version-changed is raised only while the release runs:

| Code | Cause | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | | malformed-version-floor | Anything else, such as "32", "v32.0.0", null, a number, or a component above 9007199254740991 | | unsupported-prerelease-floor | A prerelease or build suffix without a matching configured prerelease channel | | invalid-prerelease-config | A malformed channel, mismatched or missing prerelease floor, forbidden dist-tag or version-level override | | noncanonical-version | A floor is configured but the current package.json version is not a canonical x.y.z version to compare against | | current-version-changed | The release checkout's package.json version differs from the version the release was resolved from |

gitzone config show lists the configured floor.

Prerelease channels on main

A committed prerelease channel publishes a new version, including a newer major, from integrated main under a non-latest npm dist-tag. For example, to release version 6 while stable consumers remain on version 5:

{
  "@git.zone/cli": {
    "release": {
      "versionFloor": "6.0.0-beta.0",
      "prerelease": { "identifier": "beta", "distTag": "beta" }
    }
  }
}

Use gitzone release --plan, then gitzone release -y. Starting from 5.1.2, this releases 6.0.0-beta.0; subsequent releases become 6.0.0-beta.1, 6.0.0-beta.2, and so on, regardless of the Pending changelog's semver level. --major, --minor, and --patch are rejected while the channel is configured. Raising the floor selects a higher sequence or a newer version core; a floor cannot lower the current core. Components and the sequence must be safe integers. The plan and gitzone config doctor validate the channel and its floor.

Keep both settings throughout the beta. The required matching x.y.z-identifier.N floor makes earlier GitZone releases, including 8.8.2, reject the release before creating metadata instead of ignoring an unknown channel and publishing latest. Identifiers and dist-tags use lowercase letters and digits separated by hyphens and start with a letter. Dist-tags cannot be latest or a semver range. Prerelease channels and maintenance lines are mutually exclusive, and prereleases release main in place without --merge.

Every selected registry must already have a stable latest below the prerelease core. The channel's tag must be absent or point to an equal or earlier prerelease of the same identifier. Publication always passes --tag=beta in this example; there is no flag that overrides the committed channel. Verification proves the exact artifact and the channel tag (or a later sequence of the same core and identifier), and rejects a prerelease taking latest. The journal records the stable latest found before publication. A registry conflict stops the release; GitZone never repairs dist-tags automatically.

The identifier, dist-tag, and configured floor are part of the immutable release intent and journal. resume refuses changed channel configuration and resumes the recorded version. Accepted uploads are verified without publishing again. Use gitzone release resume 6.0.0-beta.0 -y after an interruption; unjournaled prerelease recover is rejected because it cannot prove the original channel. Git and npm targets are supported. Docker and Gitea asset targets are explicitly rejected in --plan and before any release side effects; disable those targets before configuring a prerelease channel.

To promote 6.0.0-beta.N to 6.0.0, remove both release.prerelease and its prerelease release.versionFloor in a reviewed commit, add the Pending release entry, and run the normal release flow. That explicit removal promotes the exact version core, without a semver-level flag. Leaving the channel configured always produces another prerelease.

Maintenance lines

A maintenance line releases an older major from its own branch under its own npm dist-tag, so that latest never moves. While main has published the next major only as a prerelease, a line may instead keep latest (see A line that keeps latest). It is opted into per branch: the maintenance branch commits a release.line block in its .smartconfig.json, and that block makes that branch, and only that branch, a release source. main never carries the block.

Worked example: @push.rocks/smartserve publishes 8.x as latest, and its 4.x line lives on the branch v4-maintenance under the dist-tag v4-lts (4.4.0). A security fix is released as 4.4.1 like this. On v4-maintenance, .smartconfig.json carries:

{
  "@git.zone/cli": {
    "release": {
      "line": { "branch": "v4-maintenance", "distTag": "v4-lts" },
      "targets": {
        "git": { "enabled": true, "remote": "origin", "pushBranch": true, "pushTags": true },
        "npm": {
          "enabled": true,
          "registries": ["https://verdaccio.lossless.digital", "https://registry.npmjs.org"],
          "accessLevel": "public"
        }
      }
    }
  }
}

Commit the fix with gitzone commit on v4-maintenance; the branch keeps its own changelog.md, whose ## Pending section feeds the release exactly as on main. Then, still on v4-maintenance:

gitzone release --plan
# source branch: v4-maintenance
# release line: v4-maintenance -> npm dist-tag v4-lts (major 4; latest is never moved)
# npm dist-tag: v4-lts
# npm dist-tags of @push.rocks/smartserve at https://registry.npmjs.org: v4-lts 4.4.0 -> 4.4.1, latest 8.2.0 unchanged
gitzone release -y

The release commits v4.4.1 on v4-maintenance, tags it, pushes exactly refs/heads/v4-maintenance and refs/tags/v4.4.1 with a lease on v4-maintenance, and publishes each package with pnpm publish <tarball> --tag=v4-lts. main is neither read nor written.

The rules:

  • release.line takes exactly branch and distTag. The branch is a canonical branch name other than main. The dist-tag is latest (see A line that keeps latest) or a tag of the line's own: lowercase letters and digits joined by hyphens that start with a letter and are never a semver range such as v4 or x, which npm refuses as a tag.
  • The release must run on the branch the line names. On any other branch, main included, the release stops before anything is created. A branch without release.line still releases only main.
  • A line publishes to Git, npm and Docker. Docker images carry only tags bound to the release version (see Docker images on a line). Gitea marks its newest release as the latest one, so a line with an enabled giteaAssets target is refused, and so are --merge and gitzone release recover. A line release records its intent, so gitzone release resume <version> -y continues it.
  • A line keeps its major. A Pending ### Breaking Changes section, --major or a release.versionFloor that changes the major stops the release.
  • Before anything is created, and again right before each upload, every package must, at every registry, already have a canonical latest with a higher major than the release, and the line's own tag must be absent or below the release version. Otherwise the release stops; a refusal right before an upload is recorded as the terminal dist-tag-conflict and nothing is published.
  • After the upload the line's tag must name the version, or a later version of the same major, and latest must still carry a higher major. A line whose version took latest is a terminal dist-tag-conflict. gitzone never writes dist-tags: the error names the exact command that restores latest, pnpm dist-tag add <package>@<previous latest> latest --registry=<registry>, where the previous latest is the version the release recorded when it claimed the upload.
  • release.targets.npm.alreadyPublished applies unchanged: identical bytes already published, with the line's tag on them and latest still a higher major, verify without an upload under success and stop under error. Identical bytes without the line's tag stay unverified; gitzone does not add the tag.
  • A line whose distTag is latest replaces the three dist-tag rules above with its own, described in the next section.
  • Private packages follow the same rules. Their dist-tags are read with the registry's authentication through pnpm view <package> dist-tags --json, which every release path (fresh, resume and recover) qualifies from pnpm view --help in both pnpm command dialects before it authenticates, reads or publishes anything.

A line that keeps latest

While main publishes the next major only as a prerelease (see Prerelease channels on main), latest still belongs to the current major, and fixes for it come from its maintenance branch. Such a line names latest as its dist-tag. Continuing the prerelease example: main has released 6.0.0-beta.0 under beta, latest is 5.1.2, and release/5.x branched from v5.1.2 carries:

{
  "@git.zone/cli": {
    "release": {
      "line": { "branch": "release/5.x", "distTag": "latest" }
    }
  }
}

The version comes from the branch's own changelog.md Pending section, as on every line; a ### Features entry releases 5.2.0:

gitzone release --plan
# source branch: release/5.x
# release line: release/5.x -> npm dist-tag latest (major 5; latest moves only while every higher major is a prerelease)
# npm dist-tag: latest
# npm dist-tags of @example/package at https://registry.npmjs.org: latest 5.1.2 -> 5.2.0, beta 6.0.0-beta.0 unchanged
gitzone release -y

Before anything is created, and again right before each upload, every package must, at every registry, show exactly that state:

  • no dist-tag carries a stable version of a higher major;
  • latest is a canonical version of the line's major, not above the release;
  • some dist-tag carries a prerelease of a higher major.

Otherwise the release stops like any line: before anything is committed or tagged, or right before an upload with the terminal dist-tag-conflict and nothing published. Publication passes --tag=latest, and verification treats latest as it does for main: it names the version or a later one. The same holds for release.targets.npm.alreadyPublished: identical bytes already published with latest on them verify without an upload. The line records no previousLatest, because it moves latest itself. Every other line rule applies unchanged: the branch, the major, Docker images only under version-bound tags (the Docker latest tag never moves), no Gitea assets, no --merge and no recover.

Once main promotes the higher major, for example to 6.0.0, the line stops in preflight because latest carries 6.0.0. Commit a dist-tag of its own on the branch, such as "distTag": "v5-lts", and the line continues under the usual rules.

Docker images on a line

A line may enable the docker target, but its images never move a tag that another release owns, such as latest, a literal like stable or a major tag like 4. gitzone does not decide this itself: it asks the project-local tSDocker for its release-version alias policy (tSDocker 3.11.0 or later, capability release-version-alias-policy), which admits a tag only when its template contains ##version##. Name the Dockerfiles Dockerfile_##version## (or Dockerfile_##version##-alpine and the like) and keep every @git.zone/tsdocker.additionalTags entry bound to ##version##, such as v##version##.

gitzone release --plan asks tSDocker (release-validate, which builds nothing and contacts no registry) for the exact tags each selected Dockerfile will publish:

gitzone release --plan
# release line: maintenance/32.2 -> npm dist-tag corestore32 (major 32; latest is never moved)
# targets: git, npm, docker
# docker alias policy: release-version (every tag is bound to 32.2.4; floating tags such as latest are refused)
# docker image Dockerfile_##version##: 32.2.4

A Dockerfile or additional tag that would publish a floating tag stops the plan and the release before anything is committed or tagged, naming the Dockerfile or tag and the tag it would publish:

Release line corestore32 publishes Docker images only under tags bound to 32.2.4, and tSDocker refused the release (INVALID_RELEASE_CONFIG): Alias policy release-version refuses this release: Dockerfile publishes the tag "latest", which is not bound to the release version; name the Dockerfile with ##version##, such as Dockerfile_##version##.

The policy is part of the journaled qualification request, so tSDocker enforces it again when it qualifies the built images, and resume continues under the same policy. A project-local tSDocker without the capability stops a line with Docker before anything is created; main releases keep every configured tag, latest included, and do not need the capability.

latest only moves upwards

The same check guards every main release: when a registry's latest is a canonical version above the release version, the release stops before anything is committed, tagged or published, and names release.line as the way to release an older major. A release that finds latest moved above it right before an upload records a terminal dist-tag-conflict and publishes nothing. A prerelease or non-semver latest and a package the registry does not know yet do not stop a main release.

A registry whose dist-tags cannot be read (an outage, a timeout, an unexpected answer) now stops a main release in preflight as well, before the release commit. This is intended: an unknown latest could be above the release, so publishing would risk a downgrade. Release again once the registry answers.

Journal compatibility

A line release journals release.line (branch and distTag) in the release identity and its intent, and its dist-tag as npm.tag. Each npm destination it claims records previousLatest, the latest version found when the upload was claimed. git.expectedRemoteMainOid keeps its name and holds the leased commit of the release branch, main or the line's branch. Journals of main releases are byte-identical to before. GitZone 8.3.x and earlier refuse to read a line journal or intent, so they can neither resume a line release onto main nor publish it under latest. A line release with Docker writes a schema 2 journal whose Docker request carries options.aliasPolicy: "release-version"; a schema 2 journal is accepted only when that policy is present exactly for a line. GitZone 8.7.x and earlier refuse to read such a journal or its intent. A line that keeps latest journals distTag: "latest" and npm.tag: "latest" and never records previousLatest. GitZone 8.9.x and earlier refuse to read its journal or intent, and GitZone 8.4.0 to 8.9.x refuse its configuration as well.

Exact artifacts and release journals

Journal schema 1 supports Git and public or private npm publication. Schema 2 adds Docker qualification, promotion, and cleanup evidence without changing the storage path or schema-1 parsing and resume behavior. Fresh and resumed npm publication dynamically qualifies the active stable pnpm version; there is no version allowlist. GitZone reads minimum-release-age and minimum-release-age-exclude through that exact pnpm invocation in the release directory, so project configuration and global policy retain pnpm's normal precedence. It verifies the selected version's publication time through the configured registry unless the effective policy disables the age delay or excludes that version. Missing timestamps follow minimum-release-age-ignore-missing-time, including pnpm's default of true; malformed policy, metadata failures, immature versions, and prerelease versions stop qualification. GitZone also verifies the required pack/publish command contract and keeps every subsequent command bound to the qualified exact version. A new pnpm version with the same capabilities needs no GitZone update once it satisfies the policy. Registries must be unique canonical credential-free HTTP(S) URLs. release.targets.npm.accessLevel is public (the default) or private; see Private npm packages.

GitZone keeps the selected pnpm version for help checks, packing, and publication through pnpm with <version>, including release recovery and resume. This avoids mixing a launcher's help output with a project-selected package-manager version. Use a standalone pnpm installation; Corepack launchers cannot run pnpm with. The normal release-age policy also applies when pnpm provisions that version.

The pnpm release tests cover known command dialects and dynamic version qualification and qualify actual packaging and script-free tarball publication with the active version. Run testUnit with each maturity-eligible standalone version and a matching project pin in an isolated checkout. For a focused matrix, invoke the project-local tstest on test/test.pnpmrelease.node.ts from a disposable directory whose package.json pins that row. Do not wrap these invocations in another pnpm with: pnpm rejects nested use. Keep the normal release-age policy for every row.

For a repository's first release, run from clean local main against an empty remote. GitZone verifies that no remote refs exist, records Git's zero object ID as the expected absent main, and atomically creates main and the release tag with an exact absent-ref lease. Concurrent creation of main rejects the push. A nonempty remote missing main, an unreachable remote, and first-release --merge are rejected. The normal release journal retains this initial identity for inspect/resume if publication is interrupted.

A selected Git target requires a canonical remote name and exactly one resolved push URL. Before any remote contact, including during --plan, GitZone rejects HTTP(S) usernames or passwords, embedded passwords, query strings, fragments, and unsupported protocols. Canonical local paths and SSH destinations may retain their required SSH username.

Every release that reaches final publication stores journal.json under the repository's Git common directory. A release that publishes the root package additionally runs pnpm pack once and stores the resulting package.tgz beside it; every tspublish module is stored as package.<sha256-of-package-name>.tgz. The v1 path names the storage format and contains all supported journal schemas:

<git-common-dir>/gitzone/releases/v1/v<version>/package.tgz
<git-common-dir>/gitzone/releases/v1/v<version>/journal.json

Frozen Gitea assets are stored beside them as asset.<sha256-of-asset-name>.bin. Together, the tarballs and assets are a record's payload.

After a fresh release, recovery or resume completes, GitZone keeps the payload of the newest three completed local version records by journal creation time and deletes the payload of older completed records. Journals and every other file of a record stay, so gitzone release inspect still lists every version. Set @git.zone/cli.release.recordRetention in .smartconfig.json to a positive integer to change this limit, for example "release": { "recordRetention": 5 }. The current run's record is always retained, even when resuming an older version. Incomplete records (including accepted uploads awaiting verification), release intents, temporary preparation directories and records locked by an active publication or resume are retained in addition to the configured count. A malformed or missing journal stops cleanup before deletion; stale record locks require the same manual owner check as other release locks. Retention affects local records only; it leaves Git tags and published releases unchanged. A pruned completed version has nothing left to resume: gitzone release resume reports that its artifacts were removed.

Pruning on demand

A repository that is not released again keeps its records until the next release completes. gitzone release prune applies the same retention at any time, through the same implementation:

gitzone release prune                  # report only
gitzone release prune --keep 1         # report against another retention count
gitzone release prune --apply          # delete the reported payloads
gitzone release prune --json           # machine-readable report

It reads release.recordRetention (default 3) unless --keep <n> overrides it, and deletes nothing without --apply. It refuses to run, with --apply or without, while the release store shows a release at work: a journal, record, creation or recovery-preparation lock (v<version>.lock, v<version>.record.lock, v<version>.create.lock, v<version>.prepare.lock), the detached-run lock (detached.lock) or a temporary preparation directory (.v<version>.tmp-*). The error lists those paths; remove a stale one only after checking that no release process is left. An attached release has no such state before it starts preparing its artifacts; prune never touches a record that release later creates.

--json prints one object, also on failure ({ "ok": false, "error": "..." } with exit code 1):

{
  "ok": true,
  "dryRun": true,
  "keep": 3,
  "root": "/repo/.git/gitzone/releases/v1",
  "records": [
    {
      "version": "35.1.9",
      "state": "completed",
      "createdAt": "2026-10-12T17:49:20.443Z",
      "completedAt": "2026-10-12T18:32:01.907Z",
      "bytes": 2125164346,
      "payloadBytes": 2125156896,
      "payloadFiles": 12,
      "action": "kept",
      "reason": "newest-completed"
    }
  ],
  "totals": {
    "records": 47,
    "kept": 4,
    "pruned": 43,
    "blocked": 0,
    "bytes": 83701754868,
    "wouldFreeBytes": 73850194634,
    "freedBytes": 0
  }
}

Records are ordered newest first by journal creation time. state is completed, incomplete or invalid (the journal is missing or malformed; createdAt and completedAt are then null). bytes counts every regular file of the record directory, payloadBytes and payloadFiles its tarballs and assets. action and reason are:

| action | reason | meaning | | --------- | ------------------ | --------------------------------------------------------------- | | kept | newest-completed | within the newest keep completed records | | kept | incomplete | not complete; resume needs its payload | | pruned | older-completed | payload removed by --apply; a dry run reports it as removable | | pruned | payload-absent | beyond retention with no payload left; nothing to free | | blocked | intent-present | a release intent for this version exists | | blocked | record-in-use | a publication or resume holds the record lock | | blocked | invalid-journal | detail names the error; --apply stops before any deletion |

totals.wouldFreeBytes sums the payloads of pruned records; totals.freedBytes is the same number after --apply and 0 in a dry run.

The canonical journal binds the release commit, annotated tag object, hashed Git destination, npm registries, per-target attempt states, and, when npm is selected, the package identity, tarball size, SHA-1, SHA-256, and SHA-512 integrity. Schema 2 also binds the exact tSDocker request, candidate identity, qualification result, ordered destination promotions, probe or promotion evidence, and terminal cleanup result. Journal updates use revision compare-and-swap under an interprocess lock. Release identity and destinations cannot change after journal creation. Qualification and promotion evidence is append-only, destination conflicts are terminal, and completed Docker state cannot regress. Inspect and resume reject malformed, future, noncanonical, duplicate-key, or merely reformatted journal JSON rather than repairing or normalizing it.

Packing must leave the release tree clean. Each registry receives that exact stored tarball through pnpm publish <tarball> --tag=<dist-tag> --ignore-scripts, so publish lifecycle scripts do not run. For a public package, success is recorded only after anonymous no-redirect probes verify version metadata, SHA-1 and integrity fields, the downloaded tarball bytes, and the release's dist-tag: latest, or the tag of a maintenance line. Each metadata request is bounded at 10 s; the tarball download, sized by the packed artifact, runs as long as bytes keep arriving, aborting after 30 s without data or once 10 s plus the time its size needs at 256 KiB/s have passed, so large tarballs with native binaries verify while a stalled registry still ends the probe as retryable. The dist-tag is satisfied by the released version itself and by any later release that has since carried the tag on (for a line tag, a later version of the same major), so superseded releases stay verifiable; an older version, a prerelease, a non-semver value or an absent tag does not. A matching pre-existing version follows release.targets.npm.alreadyPublished; conflicting bytes always stop the release, while transient responses and dist-tag propagation remain retryable.

Private npm packages

With release.targets.npm.accessLevel: "private" the journal records npm.access: "private", each registry receives the stored tarball through pnpm publish <tarball> --access=restricted --tag=<dist-tag> --ignore-scripts, and verification runs through the same qualified pnpm with the registry's configured authentication instead of anonymously. GitZone never reads, parses, logs or journals a credential: pnpm reads its own .npmrc for the destination, exactly as it does when it publishes, so authenticate at user level with pnpm login --registry=<registry>.

  • Before anything is committed, tagged or published — and again on resume and recover — pnpm whoami --registry=<registry> must succeed for every private destination. Otherwise the release stops with NpmRegistryAuthenticationError, which names the registry, the login step and the command to run next, and quotes the redacted pnpm whoami failure.
  • A destination is verified when pnpm view <name>@<version> --json reports the packed artifact's dist.integrity and dist.shasum, a dist.tarball on the registry's own origin without credentials, query or fragment, and the same dist-tag rules as public releases. pnpm has no command that downloads a tarball with the registry's authentication, so the bytes are proven by the registry's integrity record rather than downloaded.
  • A not-found answer counts as absence only when pnpm whoami confirms the authentication, because a registry answers an unauthenticated read of a private package with 401, 403 or 404 alike. whoami proves you are logged in, not that your account may read that package: a registry that answers 404 to a logged-in account without read access still looks like absence, so the release credential must be able to read the packages it publishes. Recovery checks a private version unpublished the same way.
  • An authentication failure found by the checks before and after publishing changes nothing in the journal: a pending target stays pending, and an accepted upload stays accepted and is never sent again. A credential revoked between that check and pnpm publish makes the publish command fail, and the target is recorded as failed like any rejected publish. Fix the credential and run gitzone release resume <version> -y.
  • A scope registry in an .npmrc (@scope:registry=…) overrides --registry for that scope's packages, so private publish and view commands also pass --config.@scope:registry=<registry> and always address the journaled destination.

Public releases are unchanged.

Accepted npm uploads

A registry accepts an upload before it serves it, and npmjs can need minutes or more than an hour to make the version readable. GitZone records that state as accepted: the upload was taken, its bytes are proven locally, and only an exact probe (anonymous, or authenticated for a private package) promotes it to verified. accepted is not failed. failed means the registry rejected the submission or the publish command itself failed.

Journals written before the accepted state recorded the same upload as failed with verification-inconclusive, because every earlier writer chose that cause exactly when the publish command succeeded. Loading such a journal adopts those registries as accepted, so their uploads are verified instead of sent again; a registry that failed for any other cause, or that never claimed a publication attempt, keeps its recorded failure. Loading leaves the stored bytes untouched, and the next journal transaction writes the adopted state.

After submitting, GitZone waits release.targets.npm.verificationWindowMinutes (default 15) for the version to become readable, probing with doubling backoff from two seconds up to twenty. If the window ends without a readable version, the run reports the accepted target, exits successfully, and leaves the journal incomplete; the release did not fail. A later gitzone release resume <version> -y verifies the accepted target and never republishes it. An HTTP 409 during any publish attempt, for a staged or an already published version, is read the same way: the registry holds that version, so the target becomes accepted and verification still has to prove the exact bytes.

Because Docker destinations follow verified npm publication, a Docker release whose npm target is still accepted stops after npm and is resumed from the release commit, because Docker promotion still has work to do. Only a resume with no publication work left runs from a later main.

A resume whose remaining targets are only accepted or verified publishes nothing, so it does not require the working tree to sit on the release commit. It runs from any later main that still contains that commit and still carries the release tag, both locally and on the unchanged remote; work can therefore continue on main while a registry catches up. A resume that still has publication work left continues to require the journaled release commit.

Publishing independently installable components

The npm target publishes what the project declares, without further configuration:

  • every ts*/tspublish.json descriptor that names a package, in tspublish's dependency and order sequence;
  • then the root package, unless package.json sets "private": true. A private root manifest stays the shared version and dependency catalog, and only its modules publish.

Descriptors without a name stay build-order-only folders for TsBuild. Every named module must declare "registries": ["useBase"]: GitZone publishes it to release.targets.npm.registries with the target's access level and alreadyPublished policy, exactly like the root package. Explicit, empty, and extendBase registry declarations are rejected before the release creates anything, and so are a module named like a public root package, a non-boolean "private", and an enabled npm target with nothing to publish. Each error names the file and the change that resolves it.

{
  "@git.zone/cli": {
    "release": {
      "targets": {
        "npm": {
          "enabled": true,
          "registries": [
            "https://verdaccio.lossless.digital",
            "https://registry.npmjs.org"
          ],
          "accessLevel": "public"
        },
        "docker": { "enabled": true, "engine": "tsdocker" }
      }
    }
  }
}

gitzone release --plan reads and validates the package set and prints it in publication order, for example:

plan: … core.releaseCheckout -> core.releaseCheckout.build -> core.prepareNpmModules -> core.packNpmArtifact -> core.writeReleaseJournal -> target.docker.qualify -> target.git -> target.npm -> target.docker.promote -> target.docker.cleanup
npm packages (publication order):
  @example/api-client (ts_apiclient, ts_interfaces)
  @example/service (root)

A private root prints root @example/service: private, not published instead of its line. Modules publish before the root package because a module can only depend on its siblings and on the root's dependencies, while the root package may depend on its own modules.

The normal build must generate every module's compiled folders. In the release checkout, GitZone re-reads the package set from the release commit and refuses to publish one that differs from the plan. It then uses tspublish's prepare() API to isolate the built modules beside the checkout, outside every repository, packs each module and the root package once, and removes the preparation directory.

The journal records the ordered module set and one target per package and registry: the modules first, then the root package. Each module tarball uses the canonical filename package.<sha256-of-package-name>.tgz beside journal.json; the root package keeps package.tgz. Every package uses the release version, and sibling dependencies use that exact version. Git is published before any npm package. All stored hashes are checked before publication; completion requires every package at every registry to verify. If publication fails partway through, release resume <version> -y verifies the completed targets and publishes only the missing ones from the retained tarballs, never a target that already verified. Resume never reruns discovery, preparation, builds, or packing.

npm can hold accepted uploads for publish-time scanning before they become installable. A release with modules submits every package in publication order, then waits for availability across the submitted set so scans can overlap. Command failures and artifact conflicts still stop submission immediately. Every package, tarball and dist-tag must verify before the release is complete, and Docker promotion waits until every package verified.

See npm's publish-time scanning announcement for the registry's availability behavior.

GitZone is the only publisher of these packages. A package.json script that runs the standalone tspublish publisher (without the plan or prepare subcommand) fails every release and resume that publishes npm packages, because it would upload different bytes for the same version outside the journal.

Gitea release attachments before npm

Enable release.targets.giteaAssets when public source archives, native executables, or other versioned artifacts must be available before npm packages. This works with root packages and ordered tspublish modules. The Git branch and tag target must be active; integrate main before releasing. Docker and --merge cannot be combined with this target.

{
  "@git.zone/cli": {
    "release": {
      "targets": {
        "git": { "enabled": true, "remote": "origin", "pushBranch": true, "pushTags": true },
        "giteaAssets": {
          "enabled": true,
          "apiOrigin": "https://code.example.org",
          "owner": "example",
          "repository": "native-cli",
          "tokenEnv": "GITZONE_RELEASE_GITEA_TOKEN",
          "manifestPath": "dist/release-assets.json"
        },
        "npm": { "enabled": true, "registries": ["https://registry.npmjs.org"], "accessLevel": "public" }
      }
    }
  }
}

The configured HTTPS origin must match the resolved SSH/HTTPS Git host and exact owner/repository path. SSH aliases cannot establish that identity. tokenEnv is the environment variable's name; credentials are never stored in the journal. Before Git publication, authenticated and anonymous repository checks require a public repository, repository-admin authority, and a PAT with write:repository scope (or an existing all scope). Code-push permission alone is insufficient because Gitea grants code and release-unit permissions separately. Configure an existing appropriate credential; the CLI does not acquire or broaden tokens.

The normal release build generates a strict manifest for the new release version:

{
  "schemaVersion": 1,
  "version": "1.2.3",
  "assets": [
    { "name": "source.tar.gz", "path": "dist/source.tar.gz", "size": 12345, "sha256": "<64 lowercase hexadecimal characters>" },
    { "name": "native-linux-x64", "path": "dist/native-linux-x64", "size": 23456, "sha256": "<64 lowercase hexadecimal characters>" }
  ]
}

Replace the example sizes and hashes with the actual file identities. Names and paths must be canonical and unique. Regular files and their ancestor directories must not be symlinks;