Files
Kevin Codex 530563a0bb feat(npm): publish as @twigpine/zero (#1142)
* feat(npm): publish as @twigpine/zero

The npm package moves from @gitlawb/zero to @twigpine/zero, and everything
that finds Zero's GitHub repository by name now names Twigpine/zero.

Package name
- package.json, the lockfile and release-please use @twigpine/zero.
  `repository` names Twigpine/zero: npm checks provenance against it, so a
  publish from this repo with the old URL fails with E422. The homepage is
  zero.twigpine.com.
- The name lives in package.json only. build-platform-packages.mjs names the
  wrapper, platform versions, alias names and alias targets from it, and
  bin/zero.js reads its own package.json for the alias it resolves and for
  every reinstall hint. The Go constant npmPackageName is held equal to
  package.json by a test.

Repository defaults
- The update check (DefaultRepository), install.sh, install.ps1, the npm
  wrapper's fallback downloader, and the GitHub Action's zero-repo default
  all used Gitlawb/zero. That name now belongs to an unrelated GitHub org, and
  these paths download and run what they find there, so they point at
  Twigpine/zero. Docs, README and issue-template links follow. The Go module
  path (and so `go install github.com/Gitlawb/zero/...`) is unchanged here.

Publishing
- The two inline publish steps become scripts/npm/publish-packages.sh, which
  reads the name from the assembled payloads, refuses to start when the
  wrapper's aliases do not match the platform payloads, skips versions
  already on the registry (so re-running a half-finished job is safe), and
  publishes with --ignore-scripts. The dist-tag rules are unchanged.
  internal/npmwrapper/publish_test.go runs it against a fake registry.

Docs explain the switch for existing installs: uninstall @gitlawb/zero first,
because both packages own the `zero` bin and npm refuses the second (EEXIST).

* feat(npm): publish 0.10.0 once more as @gitlawb/zero to point users at the new name

Deprecating @gitlawb/zero alone would strand every existing npm install.
0.9.0's `zero upgrade` runs `npm install -g @gitlawb/zero@latest`; with
nothing newer under that name npm installs nothing, prints no deprecation
warning (it only warns when it installs something), and Zero then reports
"updated via npm to 0.10.0" while staying on 0.9.0.

So this release goes out once more under the old name, as a bridge:

- The publish job assembles the same verified artifacts a second time with
  the new `build-platform-packages.mjs --package-name @gitlawb/zero` and
  publishes them with publish-packages.sh, after the @twigpine/zero publish.
  A gate runs it only while @gitlawb/zero's latest is still 0.9.0 (or is
  this version, on a re-run), so it happens once.
- The build knows its name from the package.json beside it. The Go side
  reports InstallMethodNpmLegacy: `zero upgrade` refuses with the switch
  steps before any release check (so it works offline too), and
  `zero update --check` adds them whether or not an update is out.
- bin/zero.js prints the same steps after zero exits (the TUI would hide a
  notice printed first), only when stderr is a terminal, and not for
  `update`/`upgrade`, which print it themselves.
- Updating from 0.9.0 now installs a real version, so npm also shows the
  deprecation message.

The steps say to uninstall @gitlawb/zero before installing @twigpine/zero:
both own the `zero` bin, npm refuses the second install with EEXIST, and a
forced install loses the bin when the old package is later removed (checked
against npm 11.16).

docs/NPM_PACKAGING.md has the maintainer runbook: bootstrap and trust the new
package, retarget the old one's trusted publisher for the bridge, then
deprecate @gitlawb/zero, revoke its publisher, and delete the bridge steps.

* fix(npm): only skip a publish when npm says the version is not there

publish-packages.sh treated any failed `npm view <name>@<version>` as "not
published". A transient network, auth or registry error on the re-run that the
skip exists for therefore published a version that was already out, and npm
rejected it with EPUBLISHCONFLICT; with the registry unreachable it published
blind. npm 11 reports every kind of missing (no such package, no such version,
including platform versions) as E404, so only E404 now means "not published".
Any other failure is retried three times and then stops the job before it
publishes anything, with npm's own error shown.

The test fake now answers a missing version with E404 the way npm does (it
returned an empty success before), and can fail lookups with a network error.
The two new tests fail on the old lookup: one with EPUBLISHCONFLICT after
transient failures, the other by publishing while the registry is unreachable.

Also renames the unused retry variable in the legacy-name gate step to `_`
(shellcheck SC2034), matching the script.
2026-10-15 11:05:51 +08:00

6.5 KiB

Update Flow

zero update --check checks the latest GitHub release and compares it with the local CLI version. zero update --apply (or its shorthand, zero upgrade) downloads, verifies, and installs it.

zero update --check
zero update --check --json
zero update --check --repo Twigpine/zero
zero update --check --target windows-x64

zero upgrade
zero update --apply

--check and --apply are mutually exclusive. zero update requires one of them explicitly; zero upgrade is zero update with --apply implied.

--check is check-only:

  • It does not replace the running binary.
  • It exits with code 0 when the check succeeds, even when an update is available.
  • It exits with code 1 when the release check cannot be completed.
  • --json prints the same result in a machine-readable format for scripts and CI.

--apply installs the update in place:

  • npm installs delegate to npm install -g @twigpine/zero@latest. An install of the old @gitlawb/zero package refuses, without a release check, and prints the steps to switch to @twigpine/zero (see INSTALL.md); --check prints them too.
  • Standalone installs download the release archive, verify its checksum, extract it, and atomically replace the running binary plus any installed optional sandbox helpers.
  • On Windows, the running executable is renamed aside and cleaned up on the next zero update --apply or zero upgrade, since it can't be overwritten while running.
  • --target cannot be combined with --apply; it only applies to --check, since applying always installs onto the current machine.
  • --repo and --endpoint are ignored when applying to an npm-managed install: that path delegates to npm install -g @twigpine/zero@latest and takes its release from the npm registry, not from GitHub. They still apply to --check there.
  • --json serializes Zero's final result. For npm-managed installs, npm may also write progress output to stdout, so neither zero update --apply --json nor zero upgrade --json is guaranteed to produce a single parseable JSON document. Use --check --json for machine-readable automation.

Useful flags:

Flag Purpose
--repo <owner/repo> Use another GitHub repository for --check and --apply/upgrade. Ignored by an npm-managed apply.
--endpoint <url|owner/repo> Use a specific release API URL or repository slug for --check and --apply/upgrade. Ignored by an npm-managed apply.
--timeout <duration> Override the default release check timeout.
--target <platform-arch> Validate release metadata for another supported target (--check only).

Supported targets are linux-x64, linux-arm64, macos-x64, macos-arm64, windows-x64, and windows-arm64. Without --target, Zero checks the current platform.

Endpoint resolution order:

  1. --endpoint
  2. ZERO_UPDATE_RELEASE_URL
  3. --repo
  4. https://api.github.com/repos/Twigpine/zero/releases/latest

Installer scripts download the matching release asset for the local platform and verify its .sha256 file. If Zero is already installed, run zero upgrade instead of reinstalling.

Windows recovery state (standalone installs)

This section describes Windows only. On Linux and macOS a standalone update renames the staged file directly over the executable path through the installation directory's file descriptor, so the replacement is atomic and no aside copy, marker, or recovery record is ever created — a failed update leaves the previous binary in place and nothing to resolve.

On Windows, a running executable cannot be replaced in place, so the previous binary is moved aside to <binary>.zero-update-<random>.old first. The updater records that exact file — bound to its filesystem identity, in per-user state outside the installation directory — and deletes only that recorded copy after the next update is verified in place. Backups it did not create are never removed, so a file such as zero.exe.before-manual-patch.old is left alone. A recorded copy that something else holds open (an on-access scanner, an editor) stays recorded and is removed by a later update instead.

An update refuses to run while unresolved recovery state exists beside the binary:

State on disk Meaning
<binary>.old (or <binary>.<suffix>.old) plus a .keep marker, or a per-user update-recovery record naming that copy A previous update could not restore the original binary. The .old file may be the last binary the updater verified; the installed one may be unverified. The .keep marker is in the installation directory and can be deleted; the identity-bound record is not, so deleting the marker alone does not end the refusal.
<binary>.…old.<suffix>.recovery A previous update could not even write the marker, so it moved the last verified binary to that name.
The binary is missing and one or more *.old files exist The previous attempt was interrupted between moves.

The refusal names the paths involved and the two moves that end the state: either move the recovery binary back over the executable path, or — if the installed binary is the one you want — delete the recovery copy itself (not only the .keep marker) after verifying the installed binary, then update again. Deleting the marker alone is not enough: a failed restore also records that copy in per-user state outside the installation directory, and the next update consults that record even if the marker is gone.

That trusted record lives under %AppData%\zero\update-recovery (per-user). The recovery copy and its .keep marker sit in the installation directory (per-machine). A second Windows account on the same machine does not see the first account's unresolved record; the sibling marker is the machine-visible signal for every account. There is no separate machine-wide store: writing one would need a shared writable location and ACL rules the updater does not own.

This is deliberately fail-closed and differs from older releases, which deleted <binary>.old and proceeded. The trade-off is that anyone who can write in the installation directory can plant <binary>.old and <binary>.old.keep there and make every subsequent update refuse until an operator clears them. That is the safe direction: the alternative is an update that overwrites the only verified copy of the previous binary. If updates start refusing, inspect those files before removing them — an installation directory that a lower-privileged account can write to is itself worth fixing.