gnopm

A package manager for gno workspaces.

CI Go Reference Latest release License GitHub

go install moul.io/gnopm@latest

The version belongs in gnomod.toml

A gno package path ends in its version, so the obvious layout gives each version its own directory. Bumping then means copying the directory and editing the copy.

git cannot pair a copy. The review diff of a version bump becomes a set of added files with no content diff at all, which is backwards: a bump is by definition the compatibility change that most needs reviewing, and it is the one change you cannot see. git log --follow, blame and bisect all stop at the copy too. One real port of 25 realms landed as +11,103 / −0 across 112 files, with every behaviour change invisible.

gnopm removes the copy. The toolchain already reads the version from the module line, so the directory does not have to repeat it.

p/alice/md/gnomod.toml     module = "gno.land/p/alice/md/v1"
p/alice/md/md.gno          edited in place

What that looks like

gnopm bump changes one line in gnomod.toml, and git records a real diff
A bump is one line in gnomod.toml. Then you edit the files, and git diffs them.
gnopm status
status to ask, sync to fix. Those are the two you type.
gnopm ls showing where each version's source is
Versions with no directory left are rebuilt from the commit that still holds them.
gnopm doc on a version that has no directory, and the two signatures of a bump side by side
gnopm doc documents a version that has no directory anywhere, which is the one somebody is asking about when they meet an old import. Put the two signatures of a bump next to each other and that IS the compatibility diff. No gno toolchain needed: gno source is Go syntax, so the standard library parses it.
gnopm publish showing live, parked and absent packages
publish says what is live, parked or absent on the chain your paths point at, and sends the rest in dependency order. It holds no key and signs nothing: gnokey does, and it prompts you.
gnopm verify refusing a pin that a squash merge would strand
Most repositories squash-merge, so a version pinned to a branch commit stops resolving the moment the change lands. verify catches that while it is still cheap.
gnopm deversion -n printing the migration plan
Migrating an existing repository is one command, and it shows you the plan first.

Every image above is generated from real command output by a script in the repository, so none of them can quietly stop matching what the tool prints.

Design, in five claims

Detect, do not ask

If gnopm can work something out, it does. A flag is an admission that it could not.

A build never mutates the lock

Only sync and bump write.

The lock is source

A change that bumps a version carries the pin that keeps the old one resolvable.

Comments are not data

Making generated prose load-bearing would break every lock over a wording change.

No resolver, ever

A gno import path contains its version, so there is no constraint to solve. gno skipped the class of problem that produces SAT solvers. Keep not solving it.

Not a wallet

It reads chains and generates commands. It never signs and never holds a key.

Coming from npm, cargo, pip or go

gnopm init                      # nothing else creates a gno workspace
gnopm search avl                # the chain is the registry
gnopm get gno.land/p/nt/tinyavl/v0
gnopm tree

Three things are missing on purpose, and each removes a class of bug the other ecosystems still fight. No resolver: an import path contains its version, so two versions are two paths and there is no constraint to solve. No registry: the chain is the registry, so nothing can be operated, ranked, spammed or taken offline, and a search cannot disagree with what an import resolves to. No lockfile drift: a published path can never be redefined or withdrawn, so a content hash pins something that genuinely cannot change.

Type npm install's verbs at gnopm and it will tell you this itself rather than saying "unknown command".

Try it

gnopm init        # make this directory a workspace
gnopm status      # what resolves, and whether anything is out of date
gnopm sync        # make the state good
gnopm bump md     # promote a package, in place; then edit the files
gnopm unbump md   # take a number back, while nothing has published it
gnopm ls          # every module and where its source is
gnopm doc md      # the documentation, for any version, directory or not
gnopm tree        # the dependency tree, in the terminal
gnopm search avl  # find a published package on the chain
gnopm outdated    # which imported versions have a newer one
gnopm why md/v0   # who still imports this version
gnopm verify      # prove every pinned version still reproduces (for CI)
gnopm tidy        # make the whole workspace right, chain included
gnopm publish     # what is missing on chain, in dependency order

A version number is a tag, not a counter

A number only means something once the version is published: until then it names no bytes, resolves for no importer, and derives no address. So the rule is bump when the last version shipped, and edit in place when it did not.

Getting it wrong is the default behaviour of a stack of pull requests. The first bumps v0 to v1 and lands. The second is written against it, sees v1 taken, and goes to v2. Both merge before either is published, and the package reaches a chain as v2 with v1 existing nowhere. Often the lock cannot even show it: bump will not pin a version living on no commit the default branch has, so the number is skipped rather than recorded.

bump stays offline and literal, because a tool that phones home before doing what it was told is a tool you cannot use in CI. Asking it to decide is opt-in:

gnopm bump md -if-published   # bumps only if v0 is live or parked on the chain
gnopm unbump md               # the inverse: lower to the lowest number nothing has taken
gnopm tidy                    # find every number spent on nothing

unbump reads its target off the chain rather than out of the lock, which is what makes it safe rather than merely guarded: the target is by construction a number the chain has never seen, so it cannot withdraw a published version and cannot redefine one either.

Migrating a repository that still has versioned directories:

gnopm deversion -n    # the plan
gnopm deversion       # do it, as git renames

Using it as a library

Everything is in packages so other programs do not have to shell out: pkg/gnomodlock is the gnomod.lock format with no dependency on the CLI, and pkg/gnopm is the operations. The repository root is the command and a high-level integration test, nothing else.