Skip to content

Mono CLI โ€‹

mono-cli installs the private mono libraries โ€” mono-utils, mono-helper, mono-devextreme โ€” into your app without spending a shared GitHub token on every pnpm install.

Not stable yet โ€” do not adopt it

This page documents how it works and where it stands. It is not a migration guide. Keep installing the libraries the way Getting started describes: the catalog:internal git specs with a classic token.

The mono-* templates already run mono-cli, which is why you may see workspace:* in their package.json. That is deliberate โ€” they are where it is being proven. Leave them alone, and do not move your own project across until this warning comes off. Why it is not stable yet has the detail.

The problem it solves โ€‹

Apps pull the private libraries through pnpm git specs with a token baked into the URL:

yaml
# pnpm-workspace.yaml
catalogs:
  internal:
    mono-utils: git+https://ghp_xxxxxxxxxxxx@github.com/EJI-ICT/libs#path:/packages/mono-utils
    mono-helper: git+https://ghp_xxxxxxxxxxxx@github.com/EJI-ICT/libs#path:/packages/mono-helper

One shared classic PAT, committed in plaintext, charged against a single rate limit by every developer on every install. Anyone with read access to the repo can lift the token out of git history, and it grants whatever that token's scopes allow โ€” which for a classic PAT is usually every repo its owner can reach.

mono-cli replaces the download with a local checkout under .mono/packages/, registered as pnpm workspace projects. A developer who has been invited to the repo reaches it with their own machine credentials (Git Credential Manager, gh auth), which have no REST rate limit. The PAT becomes a fallback most people never touch.

How it works โ€‹

sh
mono-cli i
  1. git ls-remote โ€” proves access and returns the head SHA in one call.
  2. Stops there if every requested package is already at that SHA. A no-op run is about two seconds and downloads nothing.
  3. Otherwise takes one shallow checkout of the monorepo, however many packages were asked for.
  4. Copies each package's publish subset into .mono/packages/<name>/.
  5. Registers .mono/packages/* in pnpm-workspace.yaml, repoints package.json at workspace:*, and deletes the catalog entries it replaced โ€” tokens included.
  6. Runs pnpm install.

Commands โ€‹

sh
mono-cli i                                # all mono packages
mono-cli i -p mono-utils mono-helper      # a subset (dependencies pulled in)
mono-cli i --no-install                   # skip the trailing `pnpm install`
mono-cli i --dry-run                      # show every change, write nothing
mono-cli list                             # what is installed, offline
mono-cli remove -p mono-helper            # delete a package and forget it

Transports โ€‹

Tried in order. The first three cost the token nothing:

authenticates with
git clone --depth=1your own machine credentials
git init + fetch --depth=1same; survives a blocked clone, and the only path that accepts a pinned SHA
token over githttps://x-access-token:<TOKEN>@โ€ฆ, same shallow clone
REST + archive ZIPonly when git is not installed at all

All four produce a byte-identical tree โ€” core.autocrlf=false, core.eol=lf and core.symlinks=false are set on every git call so the commit cache cannot silently lie. Force one with --transport=git|token (or MONO_CLI_TRANSPORT). --transport=git disables the token fallback entirely, which is what you want in CI, where a silent fallback would hide a missing invite.

What lands on disk โ€‹

Only the publish subset โ€” package.json, whatever files lists, and README/LICENSE. mono-helper goes from 47 MB of demo/, src/, tests/ and plan/ down to roughly 4 MB of dist/.

The copied package.json has its devDependencies and scripts stripped. Every mono package ships a prebuilt dist/, so nothing needs building, and leaving the dev deps in would make your app install mono-helper's VitePress/Playwright toolchain on every pnpm install.

State under .mono/ โ€‹

file
packages/<name>/the installed packages
packages-commit-cache.jsonthe SHA each package came from; drives the skip
packages-access-cache.jsonnegative-only "this machine cannot reach the repo", 6 h TTL
packages.lockone run at a time

All of it is disposable โ€” delete .mono/ and the next run rebuilds it.

Configuration โ€‹

defaultflag
MONO_CLI_GITHUB_TOKENโ€”PAT for the fallback tiers
MONO_CLI_REPOhttps://github.com/EJI-ICT/libs--repo
MONO_CLI_REFmain--ref โ€” branch, tag or 40-hex commit
MONO_CLI_TRANSPORTauto--transport
MONO_CLI_GIT_METHODauto--git-method
MONO_CLI_PROBE_TIMEOUT_MS60000
MONO_CLI_FETCH_TIMEOUT_MS300000

Precedence is flag โ†’ env โ†’ mono.config.* โ†’ default.

Why it is not stable yet โ€‹

Not because the idea is wrong โ€” the token problem is real and this is the right shape of fix. It is that the implementation is days old and the failure modes it has already produced were destructive ones.

It is four commits old, and three of them are bug fixes. The whole package was written and patched inside one evening:

c837f4cainitial git-method resolution and materialisation
63ed1472fix: survive a directory that cannot be renamed
768ca260fix: never destroy an installed node_modules on update
2e4bf56afix: do not re-ignore .mono when the app tracks its manifests

Two of those are the kind that eat someone's working tree. An earlier build wiped node_modules during an update; another rewrote your .gitignore back to a bare .mono rule, silently undoing the negation lines that keep the tracked package manifests in git. Both are fixed โ€” but they are what "four commits old" means in practice, and nobody outside the templates has exercised the fixed builds yet.

A fresh clone cannot bootstrap itself. Once package.json says workspace:*, pnpm install fails before any script can run โ€” pnpm resolves the workspace first, so it hits ERR_PNPM_WORKSPACE_PKG_NOT_FOUND for packages mono-cli has not fetched yet. The workaround is to commit each package's manifest under .mono/packages/*/package.json, which then needs a four-rule .gitignore and a .gitattributes entry to survive Windows line endings. That is three files of scaffolding a project must get exactly right before its first install works.

It does not actually remove the token. mono-cli itself is still a plain git dependency installed from a tokenized URL, and a fresh clone needs MONO_CLI_GITHUB_TOKEN to pnpm dlx it before anything else. The shared PAT count goes from three specs to one โ€” better, not gone.

It depends on a pnpm limitation that may move. The packages are written as bare workspace:* rather than a catalog:mono entry only because pnpm 10.7 rejects every local protocol inside a catalog (ERR_PNPM_CATALOG_ENTRY_INVALID_WORKSPACE_SPEC; link: and file: are refused too, as "not yet supported"). If pnpm lifts that, the shape of the generated package.json changes again.

The tests pass, and that is not the reassurance it sounds like. mono-cli has 85 unit tests across transport, package resolution, git and rewiring, and they are green. Every bug in the table above got through them, because they are all integration failures โ€” a directory Windows would not rename, an update path that deleted node_modules, a .gitignore rewrite. Those live in the seams between the CLI and a real working tree, which is exactly what unit tests do not cover, and the only working trees it has met so far are the three mono-* templates on one developer's machine.

What to do instead, for now โ€‹

Keep the catalog:internal git specs from Getting started. If you want the rate-limit relief today without any of this, the other token-free path is stable and unrelated: mono sync already clones your federated apps with your own git credentials. That covers .mono/apps/; mono-cli is the one still settling, and it covers .mono/packages/.

Track it in the Template Changelog.