ninepm

中文文档
0.109.0发布于 2026/11/04SEE LICENSE IN LICENSE.mdTypeScriptES Module

CLI for deploying built apps to 9pm.ai.

原始使用文档
文档目录11 节
  1. Install
  2. Configure
  3. Inventory
  4. Versioning
  5. Deploy
  6. How long an app's address stays the same
  7. Pages that do not exist
  8. Access Modes
  9. Custom Domains
  10. Commands
  11. Safe API-key rotation

9pm.ai CLI

CLI for deploying built applications to 9pm.ai.

This package contains only client-side deploy code. It does not include the 9pm.ai control plane, infrastructure provisioning code, migrations, staging config, operator tokens, or smoke-test scripts.

Install

npm i -g ninepm        # persistent `9pm` binary
npx ninepm <args>      # run without installing

Configure

Authenticate in your own browser. The CLI stores the token in native protected storage: macOS Keychain, Windows DPAPI, or Linux Secret Service when available. If native storage is unavailable, plaintext file storage is opt-in with --allow-file-credential-store or NINEPM_ALLOW_FILE_CREDENTIAL_STORE=1.

9pm login
9pm whoami
9pm doctor

Running 9pm login again keeps a credential that still works instead of minting another; 9pm login --fresh replaces it and revokes the old one.

For CI, set NINEPM_API_KEY yourself in the CI secret store. --token and NINEPM_API_KEY still override the stored login token.

A key for CI or a server is minted from a signed-in machine with 9pm api-key create --name <name>; the token is printed once. 9pm api-key ls lists your active keys and 9pm api-key revoke <key-id> retires one. Each account holds at most 10 such keys at a time (login tokens do not count). Store the key under any name but NINEPM_* when it lives in a 9pm app's own environment, and pass it with --token.

Inventory

List the apps you have on 9pm.ai before deploying or modifying:

9pm apps
9pm apps --json

The slug column is the handle you pass to access, delete, and files --project.

Versioning

Check installed vs published CLI version:

9pm --version       # installed
9pm latest          # installed vs latest, prints update one-liner when behind
9pm doctor          # surfaces the same drift line alongside credential/account health

Upgrade:

npm i -g ninepm@latest

Deploy

Deploy a static directory:

9pm deploy . --name my-app --slug my-app

Deploying again under the same slug replaces the live app. To try a change without touching it, deploy under a different slug, which creates a separate app at its own address. On Free, your one app slot can't also hold a separate preview app. Deploying to your existing app updates its live version. Practice mode rehearses the current app's workflow. Testing a changed version separately requires another app slot: a plan with more apps (Hobby allows 3).

How long an app's address stays the same

Check this before printing a link or QR code. The live URL is <slug>-<suffix>.9pm.ai, and the suffix comes from the app's internal id, not from the slug:

  • Redeploying under the same slug keeps the address.
  • Deleting the app and creating it again under the same slug gives it a new address, because the new app gets a new id. Anything printed with the old one stops working.
  • There is no rename: a different address means a different app.
  • A custom domain you own, added on the dashboard's Domains page, gives the app an address of your own. Not every plan includes custom domains.

Pages that do not exist

A static site or --entry worker app answers a path that has no file in one of two ways. Every deploy prints which, as a Missing pages: line.

  • --no-spa (the default for new apps): the path returns status 404, with the deploy's own 404.html when it has one. / still serves index.html, and extensionless links work: /menu serves menu.html, or redirects to /menu/ when only menu/index.html exists, and /menu/ serves menu/index.html. Right for a multi-page site, where a removed or mistyped page should say "not found" rather than show the homepage.
  • --spa: every path with no file serves index.html, so a single-page app that routes in the browser loads on a deep link or a refresh. That includes dotted paths such as /users/jane.smith and missing files such as /missing.json, which get the app's shell rather than a 404.
9pm deploy ./dist --name my-spa --spa

The app remembers the choice: a deploy without either flag keeps it, and a deploy that fails to publish leaves the previous choice in place. Apps created before this choice existed keep serving index.html for every unmatched path until a deploy passes one of the flags. A worker app's own Response, a 404 included, is always returned as it is. The flags do not apply to --bundle or container apps.

Deploy a built framework output directory:

npm run build
9pm deploy ./dist --check
9pm deploy ./dist --name my-app --slug my-app

Deploy with a managed SQL database:

9pm deploy ./dist --name my-app --slug my-app --with-db

Attach an optional short description to the deploy (shown next to the version in the dashboard; up to 200 characters):

9pm deploy ./dist --name my-app --slug my-app --description "Fix login redirect"
9pm deploy ./dist --name my-app --slug my-app --owner acme   # into an organisation you are an admin of

The CLI prints the public app URL after upload and finalization.

Store and share working files:

9pm files put ./report.pdf
9pm files share <file-id> --expires 24h
9pm files put ./notes.md --project my-app --path notes/release.md
9pm files share <file-id> --project my-app --public

Personal files and project files both count toward the account file storage limit. Share links expire and are capped at 7 days.

Share links are owner-only by default: the link opens only for you, signed in. Add --public when the recipient has no account — that link needs no sign-in, so anyone who obtains the URL can download the file until it expires. Do not use --public for anything containing personal data, credentials or a database export.

Deploy a first-class Worker runtime app:

9pm deploy ./examples/worker-app --name worker-app --slug worker-app --runtime worker --entry src/index.js --assets public --with-db

The --runtime worker entry must be a bundled fetch-compatible JavaScript module with a default export. With --with-db, the app receives env.DB and should use SQLite-compatible prepared statements.

Choose which files are public with --assets <dir>: only the files under that directory are served, from the app's root (public/index.html answers on /), and nothing else in the deploy directory is uploaded. Use --no-assets when the entry serves every response itself. The app remembers the choice, so later deploys need not repeat the flag; pass the other flag, or another directory, to change it — there is no way back to publishing everything. A worker app that has never chosen serves every file in the deploy directory except the entry publicly, server helpers and leftovers like index.js.orig included; every deploy of such an app prints a warning naming those files, and 9pm deploy --check lists every file a deploy would make public. Its summary counts the files scanned apart from the files selected for upload (the entry and the public files); --check itself uploads nothing.

Deploy a container app from a local Docker build:

9pm deploy . --check --runtime container --port 8080
9pm deploy . --name container-app --slug container-app --runtime container --port 8080 --health-path /health --region auto

Container deploys require Docker locally. The CLI builds a linux/amd64 image, exports it, checks image size against the per-app limit and account retained-image quota, uploads through a short-lived session, and finalizes a queued deployment. The container must listen on 0.0.0.0:$PORT - always read the PORT env var rather than hardcoding the selected --port, since on some placements the platform holds the selected port and hands the app a different one; set --health-path when the app has a cheap readiness endpoint.

Container CPUs are shared by default: each one can burst well above its baseline for a while, then is throttled to a fraction of a core once that burst allowance is spent. That suits a web app. Sustained CPU work - video or audio encoding, transcoding, ML inference - wants dedicated cores: add --cpu-kind performance. Each performance CPU comes with at least 2 GB of memory, costs noticeably more, and is available only on plans that include it; a plan without it refuses the deploy with plan_container_performance_cpus_unavailable.

Use --with-db when the container needs managed SQL. The app receives NINEPM_SQL_URL and NINEPM_DB_BINDINGS; send POST requests to NINEPM_SQL_URL with { "binding": "DB", "sql": "SELECT 1", "params": [] }. Some placements also set NINEPM_SQL_TOKEN — when it is present, send Authorization: Bearer <that token> on every request, or the bridge refuses them all with 401 {"error":"unauthorized"}. Read the URL and the token from the environment on each call rather than hardcoding either: the URL differs between placements, and the token is reissued on every deploy. For local development, use the app's normal local database adapter behind a repository layer and keep deployed SQL compatible with SQLite.

Regions are provider-neutral: auto, us-east, us-west, europe, asia, south-america, middle-east, oceania, africa.

When a container app fails to come up or you want to investigate it after the fact, retrieve its recent logs:

9pm logs my-app                # recent container logs + the failure reason, if any
9pm logs my-app --lines 50     # last N lines
9pm logs my-app --json         # machine-readable

9pm logs reports the latest container deployment's status, health, the failure reason, and the captured startup output when a deploy failed to come up. It is read-only and does not affect deploys.

Delete an app, its managed database, managed container app, and retained image:

9pm delete my-app --confirm my-app

The app's address goes with it: creating an app again under the same slug gives it a new address, so printed links and QR codes stop working. 9pm delete prints this warning before it deletes anything.

9pm delete also prints Its address stops serving the app within about a minute. when the delete went the way whose timing has been measured: a static or Worker app, or a container app (including one with a volume, always-on or custom CPU/memory), with no custom domain. Not every container app qualifies, so go by the line, not the app type. When the line is missing (an app with a custom domain, some container apps, or a cleanup step that reported an error), how long the address keeps serving has not been measured, so check the address yourself.

Access Modes

Apps are public by default.

Changing an app's access mode needs --switch-mode. Without it the command changes nothing and prints what the change would do (who loses access, what happens to the app's custom domains) and the command to re-run. Service-token mode locks every person out and removes the app's custom domains; leaving it sets them up again with new DNS records to publish. Creating another service token on an app already in service-token mode needs no flag.

One-time PIN with email/domain allowlist:

9pm access allow email my-app [email protected]
9pm access allow domain my-app example.com
9pm access otp my-app --switch-mode
9pm access revoke email my-app [email protected]
9pm access revoke domain my-app example.com

Identity provider access:

9pm access idp my-app --provider google --switch-mode

Machine access:

9pm access service create my-app --name backend --switch-mode

Return to public:

9pm access public my-app --switch-mode

Create an app-scoped runtime token for server-side allowlist management:

9pm access token create my-app --name production-app-server

Use 9pm_app_... runtime tokens only from server-side code. They can list, add, and remove email/domain grants for one app. They cannot deploy, create databases, create API keys, or manage another app. Service token secrets returned by access service create are also server-side secrets.

Custom Domains

Connect a domain you own to an app:

9pm domains add my-app www.example.com

This prints the DNS records to publish at your DNS provider, with names relative to your domain (www, _acme-challenge.www) and what DNS shows for each right now. When a TXT value is missing, add it as its own TXT record; other TXT values on that name can stay. The root of your domain (@) cannot hold a CNAME, so a root domain is shown as an ALIAS / ANAME record when your DNS host is not one the CLI recognises (a recognised host's own name for it otherwise): add it as ALIAS or ANAME (or a CNAME your provider flattens). A correct ALIAS answers with addresses, which cannot be compared from outside, so that row follows the platform's own routing check. Then:

9pm domains wait my-app www.example.com

wait exits 0 once the domain is live, 10 if it timed out while you have something to do (a record is missing or could not be checked, or a CAA record blocks the certificate even though every record is found), 11 if it timed out with every record checked and found and nothing left for you (the platform is still finishing; wait again), 20 if the domain failed or expired, and 1 on a usage, sign-in or API error. Since 0.74.0 a CAA block exits 10; earlier versions exited 11. Since 0.77.0 a bare domain whose DNS host cannot point a root domain at a hostname (GoDaddy, Route 53, IONOS, Wix) makes wait stop at once with 10: its next is use_www with the www hostname to add instead, since no record at the root can finish it. 9pm domains status <app> <hostname> --json prints {hostname, app, state, reason, next, routing, tls, provider, providerId, nameservers, zone, records, advisories, error, expiresAt, checkedAt}, where provider is the DNS host (where the nameservers point, not necessarily where the domain was bought), reason says why (reason.providerMessage carries the provider's own words when it reported any, with source saying whether they are the certificate authority's), next says who acts next and what to do, and state is live, ready_to_switch, waiting_for_dns, dns_unverified (routing is active but the records could not be checked), issuing_certificate, points_elsewhere, failed or expired. Each record's ok is null when the check cannot say, and its check says why (not_checked, or apex_addresses). A record whose value was found at the name with the domain typed twice (_acme-challenge.www.example.com.example.com) has check: "doubled" and doubledAt: enter only the short name shown. Since 0.79.0, against a server that sends them: a record answered by a wildcard record of the DNS host (*.example.com) rather than one of its own has check: "wildcard" and wildcard, and the row says to add a record at that name (the wildcard can stay; a TXT value is always added beside the values already there); resolvers lists what each public resolver answered when two were asked, and when one shows the expected value and another does not, the record is check: "resolvers_disagree" with ok: null: neither cache proves which answer is current, so confirm the record at your DNS host and check again. A record that does not match carries ttlSeconds, how long that answer may stay cached. In 9pm domains check output (0.95.0 or later; the server reads the host directly only for one domain's Re-check, so list, status and wait never carry it), a record whose DNS host is GoDaddy, Namecheap, Google Cloud DNS or NS1 can carry authoritative: what the host's own nameserver answered when asked directly (outcome: "answered" with nameserver, current and matches, or outcome: "unverified" when that read failed). The row then says "your DNS host publishes …", or "your DNS host: unable to verify directly"; a failed read never means the record is missing. It only explains the row: ok, check and the state stay the public resolvers' verdict either way. Other hosts get no such field. A root answering with addresses while routing is pending asks you to confirm the ALIAS or ANAME target, since a parking page answers with addresses too. With every record found, the text names the platform's next automatic check from next.nextCheckAt. advisories lists hints that the hostname may be a typo, such as a domain that is not in DNS; the domain is added either way. Since 0.78.0 each record also carries typeAtProvider, nameAtProvider ("" means leave the Host field empty) and valueAtProvider: the record as a recognised DNS host's editor wants it. typeAtProvider is null for a root record the host cannot hold (GoDaddy, Route 53, IONOS, Wix); the text output then suggests connecting www instead. list, status and wait only read; add, check, restart, remove and move need permission to configure the app.

Since 0.89.0, against a server that sends them, two states cover moving a domain between hosts. ready_to_switch: routing and the certificate are active, so the platform could serve the domain, but its routing record still leads to another host and visitors still reach that host. The text names the record and the value to change it to, next is replace_record, and wait keeps waiting until it is live. points_elsewhere: the domain was live and its DNS now leads to another host, so the provider stopped routing it. next is remove: remove the domain here once visitors reach the new host. If the move was a mistake, change the record back and run 9pm domains check. wait stops at once with 10 on this state. The provider removes the domain from its configuration if it stays moved for 7 days. This does not affect your domain registration.

Since 0.101.0, against a server that sends it, waiting_for_reconnection ("DNS points back to 9pm; waiting for reconnection") is the way back: the routing record leads here again but the provider still has the hostname disconnected after the move. Nothing in your DNS needs changing; the text prints check first, then wait, which keeps polling through this state (timing out with 11), and then restart as a fallback for a domain that stays disconnected, not a required step. A server from before then calls the same domain failed with reason.code moved_needs_restart; this CLI reads that as waiting_for_reconnection when the record is found. When the record could not be confirmed, the state stays failed with moved_needs_restart and next.action recheck. That doesn't show whether you want the domain back: if you want to reconnect it, make sure the record points here and re-check; if you moved it to another host on purpose, remove it once visitors reach the new host. wait keeps polling through that too (timing out with 10). A blocked or deleted hostname never gets either. Either state's routing record carries elsewhere, the host it leads to. The platform reports these only on strong evidence: two public resolvers agree on a record of the name's own that leads, through its whole CNAME chain, to a host outside both your zone and the platform. A root answering with addresses, a wildcard's answer, resolvers that disagree, or a check that could not run leave the domain in the state it had (live, or failed for one the provider stopped routing). An older CLI prints these state names as they are.

9pm domains remove prints only the records the API says the domain used, each to delete only if its value is unchanged: a name that now holds a different value (the domain points somewhere new, or another service's certificate check uses _acme-challenge) is not ours, so leave it. Against a server that sends no such list it prints no deletion advice at all. When the removal is still finishing upstream (the API answers 202 with releasing: true), it says so, names no record to delete, and prints the 9pm domains list command: once the domain is no longer listed, it has stopped serving from 9pm. Such a domain shows as removing in list and status ("removing": true in --json), and wait on it exits 20. Since 0.80.0.

Since 0.86.0, a removal that finished (from remove or 9pm delete) also says that after removal, visitors may see a TLS or routing error while the domain's DNS still points at 9pm. It names no record and adds no deletion advice of its own: what may be deleted is only the cleanup list printed above it, each record only if its value still matches. A removal still finishing gets no such line, since it is told to leave its records in place. A server that sends no cleanup list still gets it.

Since 0.87.0, that line is followed by the order the staging rehearsal (#2182) demonstrated for a move to another host: point DNS at the new host, wait for the DNS TTL to pass, then remove the domain from 9pm. While caches update, some visitors may still reach 9pm or briefly see an error from the new host, and once DNS points away 9pm shows the domain as pointing elsewhere (before 0.89.0, or against an older server, as failed), which is expected. It links the custom-domain guide's "Moving a domain off 9pm" section on the site beside the control plane it talked to (staging's guide for --api-url https://api-staging.9pm.ai). A move to your own Cloudflare DNS zone was not rehearsed, so no order is printed for it; the guide says to contact support first.

Since 0.84.0, 9pm domains removed [--owner <organisation>] lists the DNS records you may delete after a removal that finished in the background: a remove that answered releasing, or a domain of a deleted app, finished later by the platform. It is the same list remove prints when it finishes itself, under the same rule (delete a record only while it still has the value shown), and the printed hint after a still-finishing removal or 9pm delete points at it, with --owner and --api-url kept. A removal is listed for 30 days after it finishes, only to the account that owned the domain, and not at all once the hostname has been added again anywhere: the server checks the certificate provider for it when you run the command, and a domain it couldn't check is named without its records, with the command to ask about it again. The server checks ten domains per page; the command reads up to 20 pages and says so if more remain. --hostname <hostname> asks about one domain only. --json prints the API's answer with its pages merged (next is null once every page was read). Against a server without the route it says it can't list any, prints no record and exits 1.

Since 0.83.0, 9pm domains cleanup <app> <hostname> lists the setup TXT records 9pm issued for a live domain and no longer needs, such as an earlier _acme-challenge value or the _cf-custom-hostname ownership record (--json prints the API's answer). Each may be deleted only while it still has the value shown; a different value at that name is not ours. The list is checked against the certificate provider when you run the command: a value the provider still asks for is never in it, a domain that is not live lists nothing, and if the provider cannot be read, or the domain changes while it is being checked, the command fails and lists nothing; run it again. Only values the API issued are candidates, never anything read from public DNS. 9pm domains status on a live domain prints the command. 9pm domains remove lists these values with the rest of the domain's records, since once the domain is removed none of them is needed. After 9pm domains restart, the values it says no longer count are worded the same way. Against a server without the route, cleanup says it can't list any and exits 1. Values replaced before the server kept them cannot be listed.

9pm domains move <hostname> --to <app> moves a domain to another app of the same account. It keeps its certificate and the DNS records already published, so nothing is validated again. Without a source app the CLI looks it up in the account; name it (9pm domains move <app> <hostname> --to <app>) to skip that. Each refusal is printed with what to do: the domain or the app is not found, the destination is at its custom-domain cap, a removal of the domain is still finishing, the domain was disconnected under a service-token app or was never provisioned (remove it and add it to the other app), or the destination is in another account. remove --json and move --json print the API's answer as it came. Since 0.80.0.

Every 9pm deploy lists the app's custom domains under its Deployed line: Also serving at https://<domain> for a live one, and for any other its state and the 9pm domains status command to run. If the domains cannot be read within a few seconds, the deploy says so in one line instead; the deploy itself is never held back by it. 9pm delete names the custom domains it removed and, since 0.80.0, the records the API says each one used (under the same value-unchanged rule); a domain still being released is reported as such, with no records to delete, and none can be listed for it later.

Commands

9pm --version
9pm deploy [directory] --name <name> [--slug <slug>] [--spa | --no-spa] [--with-db]
9pm deploy [directory] --check [--runtime worker --entry <path> (--assets <dir> | --no-assets)]
9pm deploy [directory] --runtime container --port 8080 [--health-path /health] [--region auto] [--with-db]
9pm signup
9pm login [--fresh]
9pm logout [--api-url <url>] [--local]
9pm doctor [--json]
9pm whoami
9pm apps [--json]
9pm latest [--json]
9pm files list [--project <site>]
9pm files put <local-path> [--path <remote-path>] [--project <site>]
9pm files get <file-id> [--output <path>] [--project <site>]
9pm files share <file-id> [--expires 24h] [--public] [--project <site>]
9pm files delete <file-id> [--project <site>]
9pm deploy [directory] --runtime worker --entry <path> (--assets <dir> | --no-assets) [--spa | --no-spa] [--with-db]
9pm delete <site> --confirm <slug>
9pm domains list [<app>] [--owner <organisation>] [--json]
9pm domains add <app> <hostname> [--json]
9pm domains status <app> <hostname> [--json]
9pm domains check <app> <hostname> [--json]
9pm domains wait <app> <hostname> [--timeout 30m] [--interval 20s] [--json]
9pm domains restart <app> <hostname>          # after a certificate failed, or as a fallback for a moved domain that stays disconnected; record values may change
9pm domains remove <app> <hostname> --confirm <hostname> [--json]
9pm domains move [<app>] <hostname> --to <app> [--json]
9pm domains removed [--hostname <hostname>] [--owner <organisation>] [--json]   # records to delete after a removal that finished in the background
9pm access show <site>
9pm access public <site> [--switch-mode]
9pm access otp <site> [--switch-mode]
9pm access idp <site> [--provider google|github|entra] [--switch-mode]
9pm access allow email <site> <email>
9pm access allow domain <site> <domain>
9pm access revoke email <site> <email>
9pm access revoke domain <site> <domain>
9pm access service create <site> [--name <name>] [--duration 8760h] [--switch-mode]
9pm access service revoke <site> <service-token-id>
9pm access token create <site> [--name <name>]
9pm access token revoke <site> <token-id>

Options:

  • --api-url <url> overrides NINEPM_API_URL
  • --token <key> overrides NINEPM_API_KEY
  • --allow-file-credential-store permits plaintext fallback storage for login and signup when native protected storage is unavailable
  • --name <name> sets the app display name
  • --slug <slug> sets the URL-safe app slug
  • --with-db requests managed storage for the app
  • --with-auth gives the app 9pm-managed end-user sign-in (email one-time codes) and implies --with-db. Once enabled it stays on for every later deploy of the app, with or without the flag; it can't be turned off
  • --database-name <name> customizes the requested database name
  • --binding <name> customizes the database binding name
  • --environment <name> sets deployment environment metadata
  • --runtime <static|worker|container> selects static asset hosting, app-owned Worker code, or local Docker container deployment
  • --entry <path> selects the Worker runtime server entry file
  • --assets <dir> makes only that directory of a worker deploy public (remembered for later deploys)
  • --no-assets makes no file of a worker deploy public (remembered for later deploys)
  • --spa serves index.html for every path that has no file, for single-page apps (remembered for later deploys)
  • --no-spa returns 404 for a path that has no file, with the deploy's 404.html when present; the default for new apps (remembered for later deploys)
  • --port <port> selects the HTTP port exposed by a container app
  • --health-path <path> optionally lets 9pm.ai check container health before marking it healthy
  • --region <region> optionally selects provider-neutral container placement
  • --cpus <n> and --memory <size> (512m, 2048, 4g) size a container app
  • --cpu-kind <shared|performance> picks the container's CPU class: shared (default) bursts and is throttled under sustained load; performance is dedicated cores, on plans that include it
  • --project <site> scopes file commands to an app
  • --path <remote-path> stores a file at a specific path
  • --output <path> writes a downloaded file to disk
  • --expires <duration> sets share-link expiry, capped at 7 days
  • --public makes a share link downloadable by anyone with the URL, no sign-in (share links are owner-only without it)
  • --confirm <slug> confirms app deletion by requiring the slug
  • --provider <google|github|entra> selects a configured identity provider
  • --duration <duration> sets a service token duration

Access command options:

  • --api-url <url> overrides NINEPM_API_URL
  • --token <key> overrides NINEPM_API_KEY
  • --name <name> names an app-scoped runtime token
  • --expires-at <iso-date> optionally expires an app-scoped runtime token

Safe API-key rotation

Use a full-access personal login. 9pm api-key rotate <key-id> creates a linked replacement and prints its token once. Store it securely and switch every consumer, then run 9pm api-key rotation complete <rotation-id> --yes to revoke the original. rotation show inspects deadlines; rotation cancel revokes the replacement. All support --json. Seven days is the maximum overlap, further capped by the original fixed expiry. Forgetting Complete disables the replacement and leaves the original's existing deadlines unchanged.

Ordinary api-key revoke warns if a replacement survives. For incident containment, 9pm api-key revoke <original-key-id> --both --rotation <rotation-id> revokes exactly that pair, even after Complete. Independently minted credentials and later rotations remain separate: review all keys created during a compromise. Unsupported servers fail clearly, with no mint/revoke fallback. Never put tokens in reports or logs.

版本与发布记录

ninepm 共发布 41 个版本,首次发布于 2026/07/03;过去 12 个月发布了 41 个版本。

版本发布日期距今
0.109.0latest2026/11/04今天
0.105.02026/10/306 天前
0.101.02026/10/287 天前
0.96.02026/10/278 天前
0.92.02026/10/2312 天前
0.87.02026/10/2213 天前

关于 ninepm 的常见问题

ninepm 是什么?

CLI for deploying built apps to 9pm.ai.

如何安装 ninepm?

在项目目录运行 npm install ninepm,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 9pm,可以用 npx 9pm 直接运行。

ninepm 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

ninepm 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

ninepm 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.18.1。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

ninepm 可以商用吗?

ninepm 使用 SEE LICENSE IN LICENSE.md 许可。许可条款写在包内的独立文件中,请阅读该文件确认可否商用。以上为概括,不构成法律意见,请以许可原文为准。

ninepm 还在维护吗?

最新版本 0.109.0 发布于 2026/11/04(今天);至今共发布 41 个版本,其中过去 12 个月发布了 41 个;首次发布于 2026/07/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。