@pierre/cli
v0.1.0
Published
The pierre command-line tool for Code Storage
Keywords
Readme
@pierre/cli
pierre is the command-line tool for Code Storage. Each API command calls
one operation of the HTTP API through @pierre/sdk, with the same name and the same fields as the
SDK method. The helpers store a private key and make plain Git work against every Code Storage
repository.
Docs for agents
Read the docs through the MCP server first, then as Markdown pages:
- MCP server: https://code.storage/docs/mcp
- Index of the Markdown pages: https://code.storage/docs/llms.txt
Each operation has a catalog ID, for example repos.branches.create. Search for the catalog ID in
the docs. pierre mcp prints the MCP server and the command that installs the pierre agent skill.
Install
Install the CLI, or run it with no install:
npm install -g @pierre/cli
pierre version
npx @pierre/cli versionThe CLI needs Node.js 22.23.4 or a later 22 release, 24.22.1 or a later 24 release, or 26.12.1 or
later. Run pierre auth setup-git from a global install: Git stores the path of that pierre, and
npx and pnpm dlx run the CLI from a cache that can change.
Quick start
Create a key on the Keys page of your organization dashboard, and keep the private key in your
secrets manager. Then store it, and let Git get its credential from pierre:
pierre auth login --hostname acme.code.storage --with-key < key.pem
pierre auth setup-gitCreate a repository, and push a local repository to it:
pierre repos create my-app
git remote add storage https://acme.code.storage/my-app.git
git push -u storage maingit clone https://acme.code.storage/my-app.git, git fetch, and git push now work. Git URLs
contain no credential.
CI and containers
Configure Git when you build the image. Put no secret in the image.
pierre auth setup-git --hostname acme.code.storageAt runtime, set PIERRE_PRIVATE_KEY to the private key PEM, so that no key touches the disk. The
other variables depend on the command and the host:
- Git on an
<org>.code.storagehost needs no other variable. The helper takes the organization from the host that Git sends. - An API command takes the organization from an
<org>.code.storageremote of the current clone. Outside such a clone, also setPIERRE_ORG. - A host on another domain needs
PIERRE_ORGfor Git, and alsoPIERRE_GIT_HOSTandPIERRE_API_BASE_URLfor an API command.
A job with no private key can set PIERRE_TOKEN to a token that another system signed. The CLI and
the Git helper send that token and sign nothing. The claims of the token limit what the job can do.
A job with no hosts file sets the organization and, for a host outside <org>.code.storage, the
hosts, as for the SDKs:
export PIERRE_ORG=acme
export PIERRE_PRIVATE_KEY="$(cat key.pem)"
export PIERRE_GIT_HOST=git.example.com
export PIERRE_API_BASE_URL=https://api.example.com
pierre repos list --jsonAPI commands
The command is the catalog ID with : between the resources, then the method. The flags are the
fields of the SDK method, with the wire names in kebab-case:
| Catalog ID | SDK method | Command |
| ----------------------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
| repos.list | pierre.repos.list() | pierre repos list |
| repos.get | pierre.repos.get() | pierre repos get acme/web |
| repos.branches.create | pierre.repos.branches.create() | pierre repos:branches create --repo-name acme/web --target-branch feat --base-ref main |
pierre --help lists every API command, and pierre <resource> <method> --help gives its flags,
its catalog ID, its HTTP route, and its scopes.
Singular nouns.
pierre repo listandpierre repo:branch createrun the plural commands.pierre repo viewrunspierre repos get.One argument. A command takes at most one positional argument: the resource it acts on. Its flag works too:
pierre repos:branches create featequalspierre repos:branches create --target-branch feat.The repository.
-Ris the short form of--repo-name. Inside a clone with a Code Storage remote,--repo-namedefaults to that repository, except forrepos delete:pierre repos:branches create feat -R acme/web --base-ref main cd web && pierre repos:branches create feat --base-ref mainFlag values. A boolean is
--forceor--no-force. An array takes its flag once for each item. An object takes JSON,@file, or@-for stdin, and each of its scalar fields also has a dotted flag:--author.name A --author.email [email protected].Content.
pierre repos:commits createtakes--file <path>for each file to add or change, read from the same path under the current directory, and--delete <path>for each file to delete.pierre repos:commits create-from-difftakes--diff <file>, or--diff -for stdin.Output.
--jsonprints the response body. Without it, an object prints one row for each field, and a list prints a table. A list prints one page:--limitand--cursorare the wire fields, and--paginatereads every page. A byte response, such aspierre repos:files get, writes to stdout or to--output <file>.Status. When an operation can answer with more than one success status, the output starts with the status.
pierre repos createprints201 Createdfor a new repository, and200 OKfor an existing one.Confirmation.
pierre repos deleteasks first. Pass--yeswith no terminal.
pierre api
pierre api <catalog-id> calls any operation through its SDK method, and prints the JSON response
body. As in gh api, path parameters are fields:
pierre api repos.branches.create -F repo_name=acme/web -F target_branch=feat -F base_ref=main-F key=valuesends a typed value:true,false,null, an integer,@file,@-, or text.-F key[name]=valuesets a field of an object.-F key[]=valueadds an item to an array, and so does-F key=valuefor an array field. Give the flag once for each item.-f key=valuesends a string.--input <file>(-for stdin) gives the body fields as one JSON object. Then-Fand-fset only path, query, and header fields.- A field that the operation does not have is a usage error.
Helpers
pierre auth login [--hostname <host>] [--org <org>] [--api-url <url>] [--from-file <path>] [--no-verify] --with-key
pierre auth status [--hostname <host>] [--json]
pierre auth logout [--hostname <host>] [--yes]
pierre auth token [--repo <repo>] [--scope <scope>]... [--ttl <duration>] [--hostname <host>]
pierre auth setup-git [--hostname <host>] [--local] [--read-only] [--uninstall]
pierre auth git-credential <get|store|erase> # called by Git
pierre api <catalog-id> [-F key=value]... [-f key=value]... [--input <file>]
pierre mcp [--json]
pierre version [--json]pierre auth loginchecks the key withrepos.list, then stores it under~/.config/pierre/keys/. It prints the host, the organization, the key file, and the key ID.pierre auth statusreports each host: the organization, the key source, the key ID, a live check, and the Git helper settings.pierre auth tokenprints a token fromcreateTokenof@pierre/sdk/auth.--scopetakes a scope ofSCOPESfrom@pierre/sdk/auth, and defaults togit:readandgit:write. An unknown scope is a usage error that lists the scopes ofSCOPES. A token whose scopes all cover the organization (org:read) has norepoclaim. Another token needs--repo, which defaults to the Code Storage remote of the current repository.--ttldefaults to 15 minutes, up to 1 hour.pierre auth setup-gitconfigures Git to get its credential frompierre(see Git credential helper).pierre mcpprints the docs MCP server, the Markdown index, the auth note, and the skill command, and the organization and API URL of the selected host when they resolve. It reads the MCP server fromhttps://code.storage/.well-known/mcp.json, with a 15-second timeout, and printshttps://code.storage/docs/mcpwhen that request fails.PIERRE_DASHBOARD_URLreplaceshttps://code.storage.--jsonprints a config block for an MCP client.
Global flags
| Flag | Meaning |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| --debug | Print diagnostics to stderr. Never prints a token |
| -h, --help | Show help |
| --hostname <host> | Select a tenant by its Git host. Overrides PIERRE_GIT_HOST and the current repository's remote. |
| --json | Print the JSON response body |
| --no-input | Fail instead of prompt |
| --quiet | Print only results and errors |
| -v, --version | Print the CLI version |
| --yes | Skip confirmations |
Exit codes
The exit code comes from the class of the SDK error, never from the message text.
| Code | Meaning | | ---- | ---------------------------------------------------------------- | | 0 | Success | | 1 | Another API error, such as a conflict or a 5xx, or no connection | | 2 | Usage error | | 3 | Credential problem: no key, a malformed key, 401, or 403 | | 4 | Not found: 404 |
An API error prints its status, its problem code, its detail, the request ID, and the docs of the
code. With --json, the CLI also prints the problem details body on stdout.
Configuration
Configuration lives in ~/.config/pierre/. When XDG_CONFIG_HOME is set, that directory replaces
~/.config. PIERRE_CONFIG_DIR replaces the whole path.
hosts.ymlholds one entry per Git host:acme.code.storage: org: acme apiUrl: https://api.acme.code.storage/api/v1 keyFile: /Users/me/.config/pierre/keys/acme.code.storage.pem git.example.com: org: acme apiUrl: https://api.example.com/api/v1 keyFile: /Users/me/.config/pierre/keys/git.example.com.pemkeys/<host>.pemholds the private key. The file has mode 600 and the directory has mode 700.
A host in the <org>.code.storage shape needs no --org and no --api-url. A host on another
domain needs both once, at login:
pierre auth login --hostname git.example.com --org acme --api-url https://api.example.com/api/v1 --with-key < key.pemThese values have separate roles:
| Flag | Purpose | Example |
| ------------ | --------------------------------------------------------------------------------------- | ------------------------- |
| --hostname | The Git host. It selects the saved configuration and the Git credential helper. | git.example.com |
| --org | The organization that owns the key. The CLI puts this value in the token's iss claim. | acme |
| --api-url | The REST API endpoint for the API commands, such as repos list. | https://api.example.com |
The hostname accepts an http:// or https:// prefix, but stores only the host. The API URL needs
a scheme because it sets the HTTP transport. The API URL accepts an origin or a base URL that ends
in /api/v1.
Several organizations can share one API URL. The server uses the signed token to identify the
organization. For example, acme.example.com and beta.example.com can both use
https://api.example.com, with separate organization names and keys. Get these values from the
service configuration; a custom Git hostname alone does not establish the organization or API URL.
Other clusters use a separate API URL for each tenant. Do not assume that an API URL which works for one tenant also works for another.
Use pierre auth status --hostname <host> to inspect the saved organization and API URL. If you
repeat login for a custom host, pass --org and --api-url again.
Environment variables
| Variable | Meaning |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| PIERRE_API_BASE_URL | The API base URL, for a host with no host entry. An origin or a URL that ends in /api/v1 |
| PIERRE_CONFIG_DIR | Replaces ~/.config/pierre |
| PIERRE_DASHBOARD_URL | The origin of the agent discovery documents for mcp. Default https://code.storage |
| PIERRE_DEBUG | Helper diagnostics to stderr |
| PIERRE_GIT_HOST | The Git host when no --hostname is given. It selects the host entry of that host, or else uses the variables, as the SDKs do |
| PIERRE_ORG | The org, for an environment with no hosts table or a host outside the default shape |
| PIERRE_PRIVATE_KEY | The private key PEM itself, or the PEM on one line with a literal \n for each line break. Nothing is read from disk |
| PIERRE_PRIVATE_KEY_FILE | A path to the private key |
| PIERRE_TOKEN | A JWT to send on each call. The CLI signs no token and needs no private key. The token's claims set its access |
| PIERRE_TOKEN_TTL | Helper token lifetime in seconds, from 1 to 3600. Default 3600 |
Each token that the CLI signs has the key ID as its JWT kid header. The key ID is the hex SHA-256
of the public key, and pierre auth status shows it.
Resolution rules
First match wins.
Host, the Git host:
--hostname.PIERRE_GIT_HOST.- A remote of the current repository: first a remote whose host has a host entry, then a remote
on an
<org>.code.storagehost. In each group, the remote namedstoragecomes first. The CLI does not use a remote on another host. - The only host entry, when
hosts.ymlhas exactly one. <org>.code.storage, whenhosts.ymlhas no entry, for the org fromPIERRE_ORGor theissclaim ofPIERRE_TOKEN.
With several host entries and none of the first three, the command exits 2 and lists the hosts. A host with a host entry uses its org, its API URL, and its key file. Another host uses the variables.
Credential:
PIERRE_TOKEN,PIERRE_PRIVATE_KEY,PIERRE_PRIVATE_KEY_FILE, the host entry's key file.Org: the host entry,
PIERRE_ORG, theissclaim ofPIERRE_TOKEN, the subdomain of a*.code.storagehost.API URL: the host entry,
PIERRE_API_BASE_URL,https://api.<org>.code.storage/api/v1.
Git credential helper
Code Storage Git remotes use HTTP Basic authentication: the username is the literal t, and the
password is a JWT that your private key signs. pierre auth setup-git writes these Git settings
once per host:
[credential "https://acme.code.storage"]
helper =
helper = !/usr/local/bin/pierre auth git-credential
useHttpPath = trueThe blank helper line stops Git from asking another helper, such as a keychain, for this host.
useHttpPath makes Git send the repository path, which the helper needs.
The helper command starts with an absolute path. setup-git selects it as follows:
- When a
pierreonPATHstarts this CLI, the helper is the path of thatpierre. It is a link fromnpm install -g,npm link, orbun add -g, or the script thatpnpm add -gwrites, such as~/Library/pnpm/bin/pierre. The path stays the same when the CLI or Node.js changes version. This command runsnodefrom thePATHthat Git uses. - Otherwise, the helper names the Node.js executable and the CLI script:
!<path of node> <path of dist/cli.js> auth git-credential. For example,npm install -gunder proto puts its link in a directory that is not onPATH. This command needs noPATH, but it names one Node.js version and one CLI location. Runpierre auth setup-gitagain after a change of either.
npx and pnpm dlx run the CLI from a cache that can change or expire. setup-git shows a warning
when the helper path is in one of these caches.
On each Git operation:
- Git runs
pierre auth git-credential getand writes the protocol, the host, and the path to its stdin. - The helper takes the repository from the path: it strips
.git, strips a+ephemeralor+importsuffix, and decodes each segment. - It resolves the org from the host and the credential by the
resolution rules. When
PIERRE_TOKENis set, it prints that token and skips the next step. - It signs a token with
git:readandgit:write, orgit:readonly in read-only mode, that lives for one hour by default.PIERRE_TOKEN_TTLsets another lifetime in seconds, up to one hour. - It prints
username=tandpassword=<token>. It stores nothing.
On any failure the helper prints quit=true, so Git never falls through to a password prompt, and
one line to stderr that names the cause.
store and erase do nothing, because nothing is persisted.
Remove the settings with pierre auth setup-git --uninstall.
Troubleshooting
| Message | Cause | Fix |
| ------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| credential helper: Git sent no path | useHttpPath is not set for the host | Run pierre auth setup-git |
| credential helper: No private key is configured | No key file for the host and no PIERRE_PRIVATE_KEY | Run pierre auth login --hostname <host> --with-key |
| credential helper: Cannot derive the organization | The host is outside <org>.code.storage and has no host entry | Log in with --org and --api-url, or set PIERRE_ORG |
| The private key is on one line (exit 3) | The paste lost its newlines, from a text selection or a password field | Use the dashboard copy button, and store the key in a multi-line field |
| 401 unauthorized or 403 insufficient_scope (exit 3) | The server rejected the token or its permissions | Read the server's error detail. Check the organization, key, and permissions, then use the retry command. |
| git push fails with 403 | The machine is in read-only mode | Run pierre auth setup-git without --read-only |
| node: not found or env: node: No such file | Git has no node on its PATH, for example in an app from the Dock | Start the Git app from a shell, or put node on the PATH of the app |
| Cannot find module or <path>/node: No such file | The helper names a Node.js or CLI path that an update removed | Run pierre auth setup-git again |
Rotate a key
Create a new key on the dashboard Keys page.
Store it for the same host. This replaces the key file and the host entry, so no logout is needed:
pierre auth login --hostname acme.code.storage --with-key < new-key.pemConfirm with
pierre auth status. The key ID changes and the live check passes.Delete the old key on the dashboard.
pierre auth logout deletes the selected host entry and its local key file. Use it when a machine
must stop using a key, then revoke the key on the dashboard.
License
Apache 2.0. See LICENSE. dist/cli.js bundles its dependencies, and
dist/THIRD_PARTY_LICENSES.txt gives the license of each bundled package.
