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

@haakco/hostingctl

v1.0.563

Published

Client-and-operator CLI for the hosting platform, over the public API.

Readme

hostingctl

The hosting platform's API from a terminal, for a client's laptop and an operator's — one binary, one token, the same commands. It is standalone: no checkout, no database, no cluster credentials and no Infisical. It speaks to the public API over HTTPS with a Bearer token, exactly as the web app does, and it generates its own client from api/openapi.json, so its request and response types are the contract's own.

Generation does not decide which operations have a command — a generated function nobody calls is a feature nobody can use. That accounting lives in src/commands/operation-map.ts, which names every published operation and either the route that reaches it, a shared read, a protocol consumer, a route that exists only to refuse, or the milestone that owes it a command; just cli-test fails when an operation is unaccounted for or a mapped route does not exist.

Every published operation is accounted for, and the map holds the current totals rather than this page: each one is reachable from a command, a file route a command redeems with a single-use grant it minted, a protocol consumer the API calls rather than a person, the refusal-only rollback route (the platform refuses every call, and recovery is a new reviewed release), or a billing operation the panel will own (plan 120). just cli-test fails when an operation is unaccounted for, when a route names a command that does not exist, and when a planned entry is already reachable.

Install

npx @haakco/hostingctl --version     # no install at all
npm i -g @haakco/hostingctl          # install, once

Node 22 or newer runs the CLI, and nothing else — no checkout, no database, no cluster credentials. The package also publishes a self-contained executable per platform (@haakco/hostingctl-linux-x64 and its four siblings, @haakco/hostingctl-linux-arm64, @haakco/hostingctl-darwin-arm64, @haakco/hostingctl-darwin-x64 and @haakco/hostingctl-windows-x64); that package's bin is the native binary, so a machine with no Node installs the platform package directly and needs no runtime.

Which machines it runs on

The platform package you install is the one compiled for that machine, and it is built for that machine's own baseline rather than a newer one:

| Package | Runs on | | --- | --- | | hostingctl-linux-x64 | 64-bit x86 Linux with glibc 2.17 or newer — CentOS 7, Debian 8, Ubuntu 16.04 and newer | | hostingctl-linux-arm64 | 64-bit ARM Linux with glibc 2.17 or newer | | hostingctl-windows-x64 | 64-bit Windows, as a native hostingctl.exe | | hostingctl-darwin-arm64 | Apple silicon macOS 11 or newer | | hostingctl-darwin-x64 | Intel macOS 10.12 or newer |

Those floors come from the toolchain the native payloads are built with: the Linux pair links against glibc 2.17 through Zig, and the two macOS floors are Rust's deployment targets for those architectures. The macOS packages still carry the earlier compiled bundle until the Mac builds their native payloads, so a macOS machine older than the floor above may run today's package and is not covered by this statement.

Removing it

npm uninstall -g @haakco/hostingctl     # or the platform package, if that is the one you installed
rm -rf ~/.config/hostingctl             # the stored sessions; Windows: %APPDATA%\hostingctl

If HOSTINGCTL_CONFIG_HOME or XDG_CONFIG_HOME is set, sessions live in that directory instead: remove $HOSTINGCTL_CONFIG_HOME (or $XDG_CONFIG_HOME/hostingctl).

The second line is the one that matters: an uninstall leaves the credential file behind, and it is the only thing on the machine that can still act as you.

hostingctl update brings a copy to the published version: it detects how the copy was installed, prints the command it will run, runs it, and reads the installed version again to confirm the change. hostingctl update --check reports the published version and that command without changing anything. For a global npm install the command is npm i -g @haakco/hostingctl@latest.

hostingctl --version reports the version of the copy that is running; a development build reports 0.0.0-dev.

Authenticate

A client signs in with the platform's authorization-code/PKCE flow. The CLI keeps the session in a mode-0600 credential file at ~/.config/hostingctl/credentials.json, inside a 0700 directory, and checks that mode on read — a wider file is refused rather than read. Access tokens last 60 minutes; the refresh token owns the lifetime and rotation is the mechanism, so a command spends the stored session and renews the access token silently rather than holding a long-lived credential. The protocol, its rules and the reasons for them are owned by authentication patterns — this README does not restate them.

For local work, use the development fixtures owned by test users: they exist only in local, DevelopmentSeeder creates them, and their password is public development data rather than a credential.

Sign in with the public client's registered ID:

hostingctl login --api-url https://api.example.com/api/v1 --client-id <client-id>

The browser completes sign-in and consent. Before the session is written, the API is asked who the new token is (GET /me): a token Passport minted but the API will not act on is a failed sign-in, and the working session it would have replaced is left alone.

Without --token, subsequent commands use that saved session. For automation, set HOSTINGCTL_API_URL and HOSTINGCTL_TOKEN instead. The isolated local acceptance proved real browser consent and refresh-token rotation on 2026-10-08.

Named sessions

One credential file holds several sessions. Each is a profile:

hostingctl login --profile staging --api-url https://api.staging.example.com/api/v1 --client-id <id>
hostingctl whoami --profile staging            # or HOSTINGCTL_PROFILE=staging
hostingctl auth status                          # the session a command would use, and its expiry
hostingctl auth logout --profile staging        # revoke at the API, then remove it here

Signing in makes that profile the default; --profile or HOSTINGCTL_PROFILE selects another. A file in the older single-session shape is read as the default profile and rewritten in the profile shape on the next save, so no one has to migrate by hand. A session is bound to the target it was issued for: --api-url naming a different host is refused before any request rather than spending the token there.

npm and Rust sessions during migration

The Rust CLI is still in development. Sign in separately with each client: Rust does not import npm's session document, and the two formats must not be copied or renamed into each other.

  • npm keeps all named sessions in hostingctl/credentials.json under the platform config home.
  • Rust keeps one session per hostingctl/<profile>.json. It uses default unless --profile or HOSTINGCTL_PROFILE selects another; signing in to a named profile does not change that default.
  • Rust reserves the profile name credentials so it cannot overwrite npm's document.

On Unix, both use XDG_CONFIG_HOME/hostingctl, falling back to ~/.config/hostingctl. Windows npm uses %APPDATA%\hostingctl; Rust uses the same default, falling back to %USERPROFILE%\AppData\Roaming\hostingctl. Rust additionally accepts HOSTINGCTL_CONFIG_HOME as a complete config directory, and honors its XDG override on Windows too. Rust refuses unsafe existing Unix directory/file permissions rather than silently repairing them. Native Windows credential protection and execution remain unfinished; Unix mode checks are not Windows ACL proof.

A machine with no browser

--no-browser prints the authorize URL, then reads the callback URL pasted on stdin. This is the case where the browser runs on another machine and cannot reach this host's 127.0.0.1:8477 listener. Only the exact registered redirect URI is accepted back, the state is compared, and neither the pasted URL nor its code is ever echoed. The port is fixed because Passport matches redirect URIs exactly; if 8477 is in use the CLI says which port to free instead of silently choosing another one.

The account and impersonation

auth also covers the account itself: profile, password, register, forgot-password, reset-password, verify-email, closure show|request, picture set|remove and notifications (including read <id> and read-all). Every secret comes from --<name>-file <path> or --<name>-stdin, never from an argument, and is never printed. closure request re-reads the review and sends its digest, so a decision made against a different review is refused rather than confirmed.

auth impersonate start --target <user-uuid> --company <slug> --reason <text> --store-as <profile> writes the impersonation token to its own profile. The operator's session is never replaced and is not made non-default; act as the target with --profile <name>, and auth impersonate stop ends it and removes that profile.

Repository operators can reuse the approved in-process login without copying a token:

just deploy-auth https://hosting-staging.haakdev.com OPERATOR_HOME_STAGING dev cli whoami --json

The helper supplies the CLI's documented environment variables, refuses URL/token overrides and preserves its exit status. Build the internal artifact with pnpm --filter @haakco/hostingctl build first. This adds no Infisical dependency to the standalone CLI. The credential still has only its own company authority; CLI writes require approval and --confirm.

src/live.integration.test.ts is the real call, gated on the environment:

HOSTINGCTL_API_URL=http://localhost:8000/api/v1 HOSTINGCTL_TOKEN=<token> \
  pnpm --filter @haakco/hostingctl test

Usage

hostingctl whoami            Which account the token belongs to, and what it may do.
hostingctl whoami --json     The same answer using the API's own field names.

Every command takes --json, and automation should prefer it: the field names are the API's, never a second naming invented here. Human-readable output is the default.

Read the private supplier traces for a domain with:

hostingctl registrar history <domain-uuid> --company acme --json
hostingctl registrar history <domain-uuid> --company acme --page 2 --json
hostingctl registrar supersede-transfer <domain-uuid> --company acme --operation <operation-uuid> --accept-terms --dry-run

This requires domain management permission and returns up to 100 entries from the preceding month. The API decrypts request and response bodies for this explicit read; the output can contain customer contact details and should be handled as sensitive evidence. The command prints a --page hint when older entries remain.

The command surface

hostingctl --help lists every top-level command, and <command> --help prints that command's own groups, its flags and what each one does. The whole surface, with every flag, is in the command reference, which is generated from the CLI's own metadata and compared against it in CI. The groups, in one line each:

| Command | Covers | | --- | --- | | whoami, environments, system, diagnostics | The session's identity and reach, every environment in a company, what the cluster still holds, and the platform's health answer | | watch | The same environment reads as a screen that refreshes itself. It ships in the Rust build the platform packages carry; the JavaScript bundle has no watch | | billing | The issuer, the price book, the discount codes a client can redeem, a month's run, the invoices and credit notes we issued, the client's own documents, and automatic invoice collection | | purchases | What a company was sold: the record, its size and its term | | backup-destination | The company's own bucket that also receives its backups | | secret-connection | An environment's connections to a customer's own secret manager | | storage | Purchased object storage: a capacity's price, the purchase, an increase or a resize at that price, which basis the bucket is billed on, the bucket's connection facts, its keys, objects and bindings, the downloads measured for a month, and the ceiling that caps the overage bill | | company | The company itself, its members and roles, its activity, its logo and favicon, and the invitation journey | | environment | Create, read, scale, retry; profile coverage, domains and redirects, databases and exports, configuration and secrets, services, site access, shared runtime and application, tools, tuning, retention, portable export, and the release bundle | | application | Create, rename, remove; build and deploy, build logs, runs and run logs, process schedules; recovery review and confirm; the HTTP cache and path routes; source binding; site access; the WordPress import and URL repair; the administrator credential; the component's publishable anonymous key | | access | Who may reach one environment: settings, grants, SSH keys, the shared password, and the per-user view | | site-user | The accounts that sign in to a company's sites | | keys | The account's personal SSH keys and where each is granted | | email | The mail service, its names, mailboxes, aliases, sending holds, send-as grants, IMAP sync (including CSV), and the platform's deliverability monitoring | | mailbox | Mail-client setup, the fallback recovery address and reset links for one mailbox | | dns | A managed name's zone, its zone-file export, records, signing, handover, and the reviewed template import | | domain, registrar | The names a company holds, their proofs and releases; the registrar's state, price, consent writes and supplier traces | | cancellation, deletion-lock, backups, logs, services | Cancelling an environment, arming its deletion lock, its backup tracks, its retained history, and its service inventory | | realm, repository | One environment's deployment realm; the GitHub connections an application deploys from | | platform | Platform-only work: domain claims and purge attestations, the client directory, company and account closure, alerts, the GitHub App, Yahoo feedback, and the prepared-content catalogue | | login, auth | Signing in and out; the session's own reads, the account's settings, push tokens and impersonation | | update, completion, help, reference | This machine only: update this copy, print a shell completion, and print help or the generated command reference |

Every write needs --confirm; --dry-run describes the change and sends no write. It is not a promise of silence: a command whose description needs the API's own metadata still reads it, so environment create --dry-run reads the package catalogue before it prints the change it would make.

Options: --api-url (or HOSTINGCTL_API_URL) — the API base including /api/v1, which is also the frontend's VITE_API_URL default — --token (or HOSTINGCTL_TOKEN), --json, --help.

Exit status: 0 ok, 1 refused by the API, 2 unusable — bad usage, no token, or an unreachable API. A refusal prints the API's message, its actionable detail when it sent one, and its X-Request-Id, which is what joins a failed command to the log line that explains it.

Quick application speed checks

hostingctl application speed-check <environment-uuid> <application-uuid> --company <slug>
hostingctl application speed-check <environment-uuid> <application-uuid> --company <slug> --json

The API makes three anonymous HEAD requests to the application's generated platform ingress, verifies TLS, and reports response-header latency, HTTP/redirect status and cache-header findings. The Hosting GUI's Speed check task uses the same result. No --confirm is needed.

This is not a browser audit: no bodies/assets or redirects are fetched, public DNS/CDN paths are not measured, and failed timings are null rather than zero. A fast HTTP error is not a fast page. The API contract owns the bounds and interpretation. MacNan owns scheduled, multi-day and deeper monitoring.

Watching a deployment settle

hostingctl watch [uuid…] draws a read-only screen of every environment in the company, or of just the UUIDs you name, and reads again every five seconds. It is part of the Rust build the platform packages carry; the JavaScript bundle the parent npm package falls back to does not have it. Each row is what the API last observed: the status word, the generation being applied against the generation known ready, whether that pair is live or queued, how old the observation is, and the API's own message.

| Key | Does | | --- | --- | | ↑ ↓ | Move the selection | | enter | Show or hide the selected environment's detail: both generations named, and the message untruncated | | r | Read again now instead of waiting out the interval | | c | Copy a one-line summary of the selected environment | | q, Esc, Ctrl-C | Quit and restore the terminal |

A read that fails keeps the last successful reading on screen and banners the failure with its bounded retry, because a screen that empties itself on a dropped connection takes away the reading you were watching exactly when it mattered. Nothing on this screen writes, and it asks the same read as environment show: --json prints that answer as a document for a script, and the command refuses to draw into a pipe (exit 2) rather than writing escape codes into it.

Access users

Create a native login using an existing public SSH key file, not a private key or a password on argv:

hostingctl access user add <uuid> --company acme --username acme_accept \
  --name "Acme Accept" --public-key-file ~/.ssh/id_ed25519.pub --dry-run
hostingctl access user add <uuid> --company acme --username acme_accept \
  --name "Acme Accept" --public-key-file ~/.ssh/id_ed25519.pub --confirm
hostingctl access users <uuid> --company acme --json
hostingctl access user remove <uuid> --company acme --access-user <user-uuid> --confirm

Use the native user's uuid from the listing. Usernames are 3–32 lowercase letters, digits or underscores, starting with a letter. --key-name overrides the key label, which defaults to --name. Missing, empty, oversized and private-key files are refused before any request. Writes require --confirm; --dry-run describes the change without sending it.

Mailbox declaration import

hostingctl mailbox import preview <email-domain-uuid> --company <slug> --format <csv|yaml> --file <path> reviews a bounded 64 KiB declaration through the company-authorized API. Replace preview with confirm and add --confirm to create missing mailboxes; --dry-run sends no request. Confirmation re-previews the same captured contents and submits its review token. Matching active mailboxes are unchanged; conflicts refuse the batch, and accepted creation is not native readiness.

See the CSV/YAML examples and versioned schema. Passwords, message copying and DNS changes are not part of this declaration.

Mailbox setup and recovery

Read one mailbox's mail-client settings, hand them over, manage its fallback recovery address, and issue reset links. These are the same email.view/email.manage operations the panel uses.

hostingctl mailbox setup <email-domain-uuid> <mailbox-uuid> --company acme
hostingctl mailbox instructions <email-domain-uuid> <mailbox-uuid> --company acme \
  --recipient [email protected] --confirm
hostingctl mailbox fallback set <email-domain-uuid> <mailbox-uuid> --company acme \
  --address [email protected] --dry-run
hostingctl mailbox fallback remove <email-domain-uuid> <mailbox-uuid> --company acme --confirm
hostingctl mailbox recovery-link email <email-domain-uuid> <mailbox-uuid> --company acme \
  --destination [email protected] --confirm
hostingctl mailbox recovery-link copy <email-domain-uuid> <mailbox-uuid> --company acme --confirm

setup prints the mailbox address and username, the incoming IMAP and outgoing SMTP host, port and security, the authentication requirement and the webmail URL — the deployment's named hosting.email.client_setup values (mail.hosting.haak.co IMAP 993 TLS, SMTP 465 TLS, webmail webmail.hosting.haak.co/webmail), never guessed from your domain or its MX records. Any part the deployment left unconfigured prints Unavailable.

fallback set stores an address that stays unconfirmed until its verification link is completed; fallback remove, and replacing it, revoke outstanding verification and recovery links. instructions and recovery-link email go to --recipient/--destination, or the stored fallback when omitted, and report the message as queued, not delivered. recovery-link copy prints the reset URL once with its expiry and a sharing warning; anyone holding it can reset the mailbox password, and issuing any link never changes the password. Every write needs --confirm; --dry-run describes the change without sending it.

A mailbox owner recovering access needs no Hosting account:

hostingctl mailbox recovery request --address [email protected] --confirm
hostingctl mailbox recovery verify --token-file ~/token --confirm
hostingctl mailbox recovery reset --token-file ~/token --password-file ~/password --confirm

These three are the only API commands that run with no token and no session: login establishes a session, auth register/forgot-password/reset-password are the public account routes, and help, completion, reference and update answer on this machine and never reach the API at all. Point the recovery commands at the API with --api-url or HOSTINGCTL_API_URL. The recovery token and the new password are read from a --<name>-file (a regular file no wider than mode 600) or --<name>-stdin, never from an argument, and are never printed. verify confirms a fallback address; reset sets a new confirmed password and reports that the change is recorded and applied in the background. A password reset does not change your mailbox's separate application passwords or your active sessions.

Platform domain decisions

System administrators confirm ownership of a name and move it between clients. These commands are platform-only: a company-scoped or impersonated token is refused by the API, and the CLI reports that refusal rather than working around it. Each acts on a domain claim by its own uuid.

hostingctl platform domain show <claim-uuid> --json
hostingctl platform domain attest <claim-uuid> --reason "the previous holder confirmed the move" --dry-run
hostingctl platform domain attest <claim-uuid> --reason "the previous holder confirmed the move" --confirm
hostingctl platform domain revoke <claim-uuid> --reason "the name was taken back" --confirm
hostingctl platform domain transfer <claim-uuid> --company beta --reason "client request" --confirm

show reports the live ownership decision and the recorded routing observation separately, so neither reads as the other. A transfer answers pending while the previous holder's routing withdrawal is only asked for, and completed once the move is observed; the command prints which, and re-running show watches it settle. Every decision needs a --reason; the three writes need --confirm, and --dry-run describes the change without sending it. --company on transfer is the client the name moves to.

Deployment realm lifecycle

Observe one environment's recorded deployment realm, adopt it, retry a failed convergence and record a workload's audience name. This is the operator path for the environment's CustD realm boundary.

hostingctl realm show <environment-uuid> --company acme
hostingctl realm adopt <environment-uuid> --custd-company <uuid> --owning-user <uuid> \
  --registration-policy <invite_only|open> --expected-revision <n> [--login-paused] \
  [--audiences <path>] --dry-run
hostingctl realm retry <environment-uuid> --company acme --confirm
hostingctl realm audience <environment-uuid> <workload-uuid> --company acme --audience acme --confirm

show prints what Hosting has recorded for that environment's realm — its state, expected and observed revisions, whether logins are paused and whether the routes are held — read from the deployment identity the access view already carries, so no new endpoint is added for it. Recorded convergence is not provider health, a working sign-in or a released route: ready means the recorded intent converged, and a route that is not held is not thereby proven live. --json carries the API's own deployment-identity object.

adopt records the environment's realm allocation intent, and is platform-addressed: the staff actor who may attest a CustD company's authority belongs to no company, so the token needs the platform permission for it. The authority is existing tenant identity only — the CustD company (--custd-company) and its real owning user (--owning-user) — and no provider project, environment or directory identifier is ever an input. --registration-policy is invite_only or open; --expected-revision is the revision the caller reviewed; --login-paused records paused logins; and --audiences names a JSON file of audience registrations in the API's own shape ([{"audience":"acme","publicClient":true,"redirectUris":["https://acme.test/callback"]}]), which the API validates rather than this CLI inventing registration settings. An omitted file means no recorded audience, and the API refuses a list that does not match the environment's recorded workload audiences.

retry asks the API to reconcile the realm from its recorded intent: the accepted answer records the retry, and reconciliation observes the result. audience records one application workload's realm audience name (--audience), which the adopted intent then has to match. All three writes need --confirm; --dry-run describes the change and sends nothing.

Email service reads

Read the company's business-email service and the names, mailboxes and aliases attached to it. These are the same email.view reads the panel uses; the CLI changes nothing here.

hostingctl email show --company acme
hostingctl email names --company acme --json
hostingctl email mailboxes <email-domain-uuid> --company acme
hostingctl email mailbox <email-domain-uuid> <mailbox-uuid> --company acme
hostingctl email aliases <email-domain-uuid> --company acme

show reports the service state, its entitlement and whether enrollment is available. names reports each attached name with the readiness checks the platform observed for it, kept apart from the desired status: an attached name is not a name whose mail is proven. mailboxes and aliases follow every page the API reports rather than truncating at the first, and mailbox reads one record independently of index pagination.

Mailbox import from CSV or YAML

Create many mailboxes at once on one attached name from a localPart,quota CSV or a versioned YAML document. Uploading and previewing writes nothing; the confirmed import is email.manage, the same capability as creating one mailbox by hand.

hostingctl mailbox import preview <email-domain-uuid> --company acme --format csv --file mailboxes.csv
hostingctl mailbox import confirm <email-domain-uuid> --company acme --format yaml --file mailboxes.yaml --dry-run
hostingctl mailbox import confirm <email-domain-uuid> --company acme --format yaml --file mailboxes.yaml --confirm

The --file is a localPart,quota CSV or a versioned YAML document, and --format says which of the two it is (csv or yaml), because the CLI does not guess a format from a filename; the file is read from disk, bounded, and never printed. preview classifies every declaration against the name and reports what it would create, what already matches and what is refused, without writing anything. confirm re-runs that review to obtain the digest the API requires, then creates the reviewed declarations. It needs --confirm; --dry-run describes the batch without sending it. An import is create-only: a repeat of matching declarations is a no-op, a conflicting declaration is a refusal, and a refused batch creates nothing. Creating a mailbox does not deliver its password; each new mailbox receives its credential through its own owner. Imports never configure source accounts, start a sync or change DNS.

Portable site pattern

Export one authorized environment's supported setup as YAML, then preview and import it into another environment you are authorized for. The pattern is portable setup intent with logical references, not a clone: no secret value, source-company identifier, observed readiness, message or file leaves the platform.

hostingctl environment portable export <environment-uuid> --company acme
hostingctl environment portable export <environment-uuid> --company acme --selection selection.json --out acme.yaml
hostingctl environment portable preview <environment-uuid> --company acme --file acme.yaml
hostingctl environment portable preview <environment-uuid> --company acme --file acme.yaml --bindings bindings.json
hostingctl environment portable prepare <environment-uuid> --company acme --file acme.yaml --bindings bindings.json --dry-run
hostingctl environment portable prepare <environment-uuid> --company acme --file acme.yaml --bindings bindings.json --confirm
hostingctl environment portable confirm <environment-uuid> --company acme --file acme.yaml --bindings bindings.json --confirm

export prints the pattern as YAML on stdout (pipe it, or use --out <path>). --selection is a JSON file naming the sections (hosting, domains, dns, email, access, sources) and the linked names to include; the API's own defaults apply when it is omitted, and any section it did not include is reported on stderr so the document on stdout stays byte-identical.

preview classifies every declaration against the target and writes nothing; it reports each section as create, reuse, conflict, unresolved, refused or unsupported, the declared secret names, the ones still unset and any blockers. --file is the pattern and --bindings is a JSON file whose bindings map the pattern's logical references to authorized target resources (never a name alone) and whose consents grant the consequential operations a selected section needs. Neither file carries a secret value.

The two files are small and explicit. A selection names what leaves the source:

{
  "sections": ["hosting", "domains", "dns", "email", "access", "sources"],
  "domains": ["shop.example.com"],
  "emailDomains": ["example.com"],
  "dns": ["example.com"],
  "access": true,
  "sources": true
}

A bindings file maps the pattern's own references to target identifiers and grants the consequential operations. loginKeys carries the public half of a key a declared SFTP/SSH login needs, and a consent left false keeps its operation refused:

{
  "bindings": {
    "applications": { "web": "<target-application-uuid>" },
    "repositories": { "web": "<target-repository-selection-uuid>" },
    "domains": { "shop.example.com": "<company-domain-uuid>", "example.com": "<company-domain-uuid>" },
    "emailDomains": { "example.com": "<email-domain-uuid>" },
    "services": { "cache": "<service-uuid>" },
    "siteUsers": { "[email protected]": "<site-user-uuid>" },
    "accessUsers": { "deploy": "<access-user-uuid>" },
    "members": { "[email protected]": "<company-member-user-uuid>" },
    "aliasRecipients": ["[email protected]"],
    "loginKeys": { "deploy": "ssh-ed25519 AAAA... deploy@laptop" }
  },
  "consents": { "serviceAddition": false, "emailInvitations": false, "autoDeploy": false }
}

A binding always names a target that already exists: an existing workload, the repository selection a source builds from (repositories), a company-owned website name and the customer DNS zone a record set lives in (both domains), the attached email name (emailDomains), an existing access login (accessUsers), an existing agency site user (siteUsers), the Company member a grant names (members), and any alias destination the pattern does not itself declare (aliasRecipients). A declaration that creates a resource — a new application, mailbox or login — needs no binding, and a name that merely matches a target is never authorization.

prepare records a reviewed draft without applying resource changes. It may create binding metadata and advance the configuration generation; this makes declared secret names writable. Set each unset value through the existing write-only secret control, then preview and confirm again. confirm re-checks current state, records create/reuse intent and queues normal reconciliation. It does not purchase, cut over DNS or remove anything, and its answer is accepted intent rather than a readiness claim. prepare and confirm need --confirm; --dry-run describes the change without sending it. A target that changed since the review is refused by the API rather than applied against state nobody saw.

Managed DNS and import

Inspect a managed name's zone and its standard zone-file export, and review then apply an import. The reads use dns.view; dns import is dns.manage.

hostingctl dns zone <domain-uuid> --company acme
hostingctl dns export <domain-uuid> --company acme --json
hostingctl dns import preview <domain-uuid> --company acme --file records.txt
hostingctl dns import apply <domain-uuid> --company acme --file records.txt --dry-run
hostingctl dns import apply <domain-uuid> --company acme --file records.txt --confirm

zone reports the published record sets, the desired generation and the last generation observed applied; export prints a heading and then the zone file the API rendered (its --json carries the file alone in zoneText for piping). The import --file holds one record set per line as name TTL type value — the grammar the panel's import box uses — with blank lines and # comments skipped and repeated name/type/TTL lines folded into one set with several values. The file is read from disk and never printed.

preview classifies every submitted set and writes nothing. apply re-runs that review to obtain the generation and review token the API requires, then records the reviewed sets and queues the apply pass, so an apply is desired state rather than an observed completion. It needs --confirm; --dry-run describes the change without sending it. A zone that changed since the review is refused by the API rather than applied against a state nobody saw. There is no dnssec read: DNSSEC is a write-only route in the API, and the observed mode is part of dns zone.

Creating an environment

Create an environment from a package the platform sells. The CLI reads the package catalog from the API, so it offers exactly the packages the platform offers, and it never names a cluster: placement belongs to the platform and the create request carries no cluster field.

hostingctl environment create --company acme --name "Acme Shop" --dry-run
hostingctl environment create --company acme --name "Acme Shop" --slug acme-shop --package small_sites --confirm

--name is the environment's display name. --slug is the name it answers on: omitted, the CLI derives one from the name (lowercase, each run of other characters folded to a single hyphen, trimmed, capped at 63 characters), and a slug shorter than three characters or outside lowercase letters, digits and single hyphens that start and end with a letter or digit is refused before any request. --package names one of the catalog's keys and defaults to its first; a key the catalog does not offer is refused before anything is sent.

The write needs --confirm; --dry-run describes it, reads the catalog so the package it names is the one the API sells, and sends nothing. The accepted answer is recorded desired state rather than a ready environment: reconciliation applies it afterwards, and --json carries the API's own created environment object.

Environment databases

Read an environment's own SQL identities, declare one, replace who may consume it, and retire one while keeping its data and its backups. These are the same environment-owned databases the configuration review shows; no password is read and no credential is issued here.

hostingctl environment databases <environment-uuid> --company acme
hostingctl environment database create <environment-uuid> --company acme \
  --key shop --engine mariadb --dry-run
hostingctl environment database bind <environment-uuid> <database-uuid> --company acme \
  --bind <workload-uuid>[,<workload-uuid>] --confirm
hostingctl environment database bind <environment-uuid> <database-uuid> --company acme --clear-bindings --confirm
hostingctl environment database review <environment-uuid> <database-uuid> --company acme
hostingctl environment database retire <environment-uuid> <database-uuid> --company acme --dry-run
hostingctl environment database retire <environment-uuid> <database-uuid> --company acme \
  --digest <review-digest> --generation <review-generation> --confirm

databases lists each identity with its engine, its configuration key, the applications allowed to consume it and any recorded retirement, and carries the API's canManage rather than assuming the account may write. create declares one independent, environment-owned database: --key is the stable declaration key and --engine is mariadb, postgres or mysql; --bind grants it to consuming workloads as it is made, and omitting it creates a real zero-consumer database. The API owns the key grammar and refuses a bad one.

bind replaces the consumer list. --bind <uuid>[,<uuid>…] names the new list and --clear-bindings detaches every application; one is required and they cannot be combined, so an omitted list never clears a grant by accident. Detaching stops future credential delivery only — a password an application already holds is not revoked — and the shared service refuses to detach a consumer until that workload is stopped and its environment generation has reconciled.

review reads what retiring one identity would do, what it retains, and the digest and generation a confirmation carries back. It reports the API's own blockedReason when it cannot proceed (a consumer is still attached, a retirement is already open, the environment is cancelled). Copy the digest and generation from that review to retire; the CLI sends those exact values, and the API refuses them if the inventory or generation changed. Recording the request is accepted work, not a completed retirement: it reports pending (or failed, which can be reviewed and requested again) until a reconcile has observed the SQL server refusing new logins. New logins then stop working; existing SQL sessions are not terminated and the applications using the database are neither stopped nor unbound. The database, every row, the database name, the SQL user name, the credential version and the backup track are retained — there is no delete, and no command here removes a database or its data.

All three writes need --confirm. --dry-run sends no mutation: create and bind still read the environment's current metadata so a stale change is refused rather than applied, while retire sends no request at all — it does not even read the review.

Environment configuration and secrets

Read an environment's desired state, review a YAML document against it, record a draft or apply it, and set the declared secret values it needs. These are the same routes the panel uses; no value is ever printed.

hostingctl environment configuration show <environment-uuid> --company acme
hostingctl environment configuration preview <environment-uuid> --company acme --file acme.yaml
hostingctl environment configuration save <environment-uuid> --company acme --file acme.yaml --confirm
hostingctl environment configuration apply <environment-uuid> --company acme --file acme.yaml --confirm
hostingctl environment configuration apply <environment-uuid> --company acme --file acme.yaml \
  --application-uuid <existing-workload-uuid> --confirm
hostingctl environment secret write <environment-uuid> --company acme \
  --key app_key --value-source file --value-file ~/app-key --confirm
hostingctl environment secret write <environment-uuid> --company acme \
  --key app_key --value-source generate --confirm
hostingctl environment secret import <environment-uuid> --company acme --file secrets.env --confirm
hostingctl environment secret local <environment-uuid> --company acme --out .env.local

show names the current, active and known-good revisions with each declared secret and whether it holds a value. preview classifies the document and writes nothing; --apply reviews it as an apply so the verdict is the one an apply relies on. save records a draft revision (not applied); apply records it and queues normal reconciliation. Both re-run the review to obtain the hash and generation the API requires, so a document that changed since review is refused rather than applied against state nobody saw. For a document with one application, --application-uuid explicitly adopts an existing workload in that environment. The same UUID is sent in preview and save so the reviewed hash covers the intended adoption.

A secret value is never taken from an argument. --value-source file|stdin reads it from a private file (mode 600) or --value-stdin. --value-source generate is only for the declared secret a Laravel application binds to its APP_KEY environment variable — the fixture's app_key, or a member-scoped <memberKey>_app_key — 32 random bytes, base64-encoded and base64: prefixed. The secret's own name never decides this: the binding in the reviewed document does, so generation refuses a declared secret that is not bound to APP_KEY. It also refuses to replace a value already in use unless --replace says to rotate it, because rotating it invalidates data encrypted with the old key. import sets several declared secrets from a KEY=VALUE file (mode 600) and is how an existing key is brought over unchanged. A value is read only when the change is sent, so a dry run reads nothing, and no value appears in output, logs or a dry-run description. Every write needs --confirm; --dry-run describes the change and sends nothing.

local is the opposite direction and the one to use for a development machine. It reads nothing from the platform's secret store: it writes each declared secret's own local development value — the local field in the document, a harmless default such as password — into an env file, and names the declared secrets that have none so you supply those yourself rather than discovering a blank variable at runtime. Because a stored value never appears in the document, there is no path by which a production credential can reach that file. It writes to --out (mode 600, since you may add a real value to the same file) or to stdout, and it needs no --confirm because it changes nothing on the platform.

Environment services

Read one environment's whole service inventory with the generation it was observed at:

hostingctl services <environment-uuid> --company acme
hostingctl services <environment-uuid> --company acme --json

The header names the desired and applied generation and the observed status in words — nothing observed yet when the environment has never been observed, and ready with applied M of N (change outstanding) when the live generation is behind the desired one. Each service follows with whether it is installed and running, its replicas, its reserved allocation when it has one, and its consumers.

Applications

One application's build, deployment, process runs, rename and removal:

hostingctl application list <environment-uuid> --company acme
hostingctl application show <environment-uuid> <workload-uuid> --company acme
hostingctl application deployment <environment-uuid> <workload-uuid> --company acme
hostingctl application build <environment-uuid> <workload-uuid> --company acme --source <selection-uuid> --confirm
hostingctl application deploy <environment-uuid> <workload-uuid> <release-uuid> --company acme --confirm
hostingctl application runs <environment-uuid> <workload-uuid> --company acme
hostingctl application run-log <environment-uuid> <workload-uuid> <run-name> --company acme
hostingctl application schedule-run <environment-uuid> <workload-uuid> <schedule-key> --company acme --confirm
hostingctl application runtime <environment-uuid> <workload-uuid> --company acme --mode stop --confirm
hostingctl application update <environment-uuid> <workload-uuid> --company acme --label "Shop" --confirm
hostingctl application removal <environment-uuid> <workload-uuid> --company acme
hostingctl application remove <environment-uuid> <workload-uuid> --company acme --confirm

deployment reports the running, requested and interrupted builds and releases, the bound source and whether a push would deploy. build queues a build of the application's applied configuration from exactly one of --source <selection-uuid>, --source-upload <upload-uuid> or --ssh-source <connection-uuid> (or the recorded binding when none is named), resolved to an exact commit or measured archive digest by the source service rather than named here. deploy makes an existing release the one the application runs. runs and run-log read the process run history and one run's own output, which may report unavailable when the cluster cannot answer rather than an empty body. schedule-run runs a declared schedule once, now, beside the schedule's own runs; it does not execute an arbitrary command, for which the API has no route. update renames the label only, never the immutable name. runtime records one application's desired runtime from --mode stop|start|restart: it reads the environment's current generation and sends running: false for stop or running: true for start and restart. Starting or restarting records a fresh runtime revision even if the application is already running, and stopping is recorded as desired intent that reconciliation observes afterwards — the accepted answer is not an observed stopped state. --dry-run makes no request at all, and a moved generation is the API's own conflict rather than a silent refetch.

removal reviews what removing one application would affect; remove re-reads that review and sends its own inventoryDigest, so a confirmation approves an exact inventory rather than a label and the environment generation makes a stale review a conflict. Removal withdraws the runtime through normal reconciliation and deletes no retained data; retiring an environment-owned database's data is a separate decision. Every write needs --confirm; --dry-run describes the change and sends nothing.

Uploaded and Git-over-SSH source

Both the Rust binary and the TypeScript CLI support these commands. All writes need --confirm; --dry-run sends nothing, and a dry-run upload does not open the file.

hostingctl application source-upload <environment-uuid> <workload-uuid> --company acme --file project.zip --confirm
hostingctl application source-uploads <environment-uuid> <workload-uuid> --company acme
hostingctl application source-upload-show <environment-uuid> <workload-uuid> <upload-uuid> --company acme
hostingctl application source-upload-discard <environment-uuid> <workload-uuid> <upload-uuid> --company acme --confirm
hostingctl application build <environment-uuid> <workload-uuid> --company acme --source-upload <upload-uuid> --confirm
hostingctl application source-ssh-list --company acme
hostingctl application source-ssh-show <connection-uuid> --company acme
hostingctl application source-ssh-create --company acme --name Project \
  --repo [email protected]:team/project.git --ref main --confirm
hostingctl application source-ssh-confirm-host-key <connection-uuid> --company acme \
  --fingerprint SHA256:verified-out-of-band --confirm
hostingctl application source-ssh-verify <connection-uuid> --company acme --confirm
hostingctl application source-ssh-update <connection-uuid> --company acme --ref v1.2.0 --ref-kind tag --confirm
hostingctl application source-ssh-rotate-key <connection-uuid> --company acme --confirm
hostingctl application source-ssh-revoke <connection-uuid> --company acme --confirm
hostingctl application source-bind <environment-uuid> <workload-uuid> --company acme --ssh-source <connection-uuid> --confirm
hostingctl application build <environment-uuid> <workload-uuid> --company acme --ssh-source <connection-uuid> --confirm

An upload is staged and reviewed, not built or deployed. ZIP bytes are streamed with their original filename; review, expiry and digest come from the API. Build explicitly, then deploy the candidate with application deploy. Discarding staging does not remove source already copied into a build.

Creating an SSH source returns its public deploy key and the discovered server fingerprint. Install the public key on the Git server, compare the host fingerprint independently, confirm that fingerprint, then verify access. No private key leaves the platform. An update changes only the label or branch/tag ref; rotation clears verification and requires installing the replacement public key. Revocation stops future fetches and candidate promotion, not the serving release. SSH sources are company-scoped, so their commands need no environment or application UUID. source-bind accepts exactly one of --selection or --ssh-source.

Retained log history

Read the platform's bounded history for one of your resources. This is retained, sanitized operational history, not the live container tail:

hostingctl logs environment <environment-uuid> --company acme
hostingctl logs dns <domain-uuid> --company acme --json
hostingctl logs mail <email-domain-uuid> --company acme
hostingctl logs environment <environment-uuid> --company acme \
  --start 2026-10-14T11:00:00Z --end 2026-10-14T12:00:00Z --limit 100

environment, dns and mail take the environment, DNS domain and mail domain uuid. The API bounds every read: at most a six-hour window within the last 24 hours, defaulting to the most recent hour, and 1–200 entries, defaulting to 100. --start and --end are ISO instants and --limit is a whole number; the server enforces the window and the CLI refuses a malformed --limit before asking.

A source of unavailable means the platform could not read the retained history — no configured reader, an active ownership interval that cannot be proven, or a boundary failure for an authorized resource. An out-of-company UUID is refused before reading history. Unavailable is not the empty result of a successful read that matched nothing; the human output distinguishes these states. The environment family reads dedicated web-container output; DNS reports zone-transfer initiation, not full query history; and mail reports a Postfix delivery attempt, which is transport acceptance rather than proof of mailbox receipt.

Automatic invoice collection

Read whether the platform may collect automatically, where one client's collection stands, and run the writes that authorize a method, withdraw it, reconcile a charge nobody can account for, and record money received outside Hosting.

hostingctl billing collection status
hostingctl billing collection show --company acme
hostingctl billing client collection --company acme
hostingctl billing collection setup --company acme [--replaces-method <uuid>] --dry-run
hostingctl billing collection revoke --company acme --method <uuid> [--reason "card lost"] --confirm
hostingctl billing collection reconcile not-charged --company acme --attempt <uuid> \
  --note "checked the provider dashboard: no transaction" --confirm
hostingctl billing collection reconcile charged --company acme --attempt <uuid> \
  --note "provider shows a completed transaction" --transaction <provider-ref> \
  --amount 1150.00 --received-at 2026-12-01T10:00:00Z --confirm
hostingctl billing collection receipt --company acme --invoice <uuid> --transaction <bank-ref> \
  --amount 1150.00 --received-at 2026-12-01T10:00:00Z --note "bank line 42" --confirm

status reports whether collection is on and ready, and names the configuration items still missing — names, never values. show reads one company's standing through the platform owner, because a staff account is not a member of the company it is looking at; client collection reads the same standing through the customer route the caller's own token can reach. Both print the recorded method and each invoice's derived position, its next collection and any recorded reason a send is blocked.

setup records the consent and creates a method that is pending: nothing is authorized, and no invoice can be charged, until the provider confirms the card in a browser. The provider's signed form is deliberately not printed — its fields carry the merchant's own credentials — so a terminal starts the authorization and the browser completes it. revoke stops future charges immediately; a charge the provider has already accepted may still settle, and the standing shows it as it happens.

reconcile records what happened to a charge nobody can account for: not-charged makes the invoice collectable again, and charged records the settlement from the provider's own transaction id, which is what makes recording the same one twice the same fact rather than a second payment. receipt records money received outside Hosting against one invoice, attributed to the operator who verified it. Both require --note: what was checked and where. That is the point — a settlement nobody had to explain cannot be recorded from here, which is why receipt refuses a missing note even though the API would accept a null one.

Every write needs --confirm; --dry-run describes the change and sends nothing. No command prints a token, a provider method handle or a credential.

What it never does

No cluster or database access, ever. Those credentials belong to the platform, and a client's laptop is not where they live. Any rule the CLI appears to need — what a complete resource profile is, which statuses exist, who may see an environment — comes from the API's own answer, because a CLI that re-derives platform rules becomes a second owner of them.