@antasphere/cli-core

中文文档
0.6.0发布于 2026/10/13MITTypeScriptES Module零依赖近 30 天下载 2755

Shared CLI foundation for Antasphere tool CLIs: the ~/.config/antasphere config home, profiles that are instances (with the implicit cloud profile), the flag → env → profile → cloud URL resolution, the per-command organization override, the instance email

原始使用文档
文档目录7 节
  1. The config home
  2. Profiles
  3. Surface
  4. Exit codes
  5. Consuming it
  6. Release
  7. Development

@antasphere/cli-core

The shared foundation every Antasphere tool CLI builds on. One config home for the whole ecosystem, per-tool profile namespaces where a profile is an instance, the flag → env → profile → cloud URL resolution order, the per-command organization override, the instance email-OTP auth client, the hub → tool connect exchange with its connect-on-demand seam and the stranded-key recovery, and the CliIo prompt seam. Tool commands, wire types, and API surfaces live in the tools — this package carries only config/login plumbing, never secrets.

The model: one identity at the hub (antasphere login, a user-scoped ant_ key); per-tool credentials minted from it by the exchange (POST {hub}/api/v1/sso/tool-token, then POST {tool}/api/v1/sso/cli-connect — the exchange names no organization, hub ADR 014); the organization a per-request parameter (the hub's server-side default organization, or --org <hub org id or name> on one command, which the tool maps to its own workspace through central_account_id). Every tool CLI ships its cloud URL and resolves it last, so antasphere login then slideless list works on a clean machine.

Consumers today: the antasphere account CLI (the hub's packages/cli), the slideless CLI and the hackathon CLI. The design decisions live in the hub architecture brief (hub-architecture.md §8, CLI family; §4c, the CLI token flow).

The config home

$XDG_CONFIG_HOME/antasphere/ (default ~/.config/antasphere/), directories 0700, files 0600. One file per tool namespace — tools never collide, and a new tool is a new file, never a schema change:

antasphere/                     0700
├── config.json                 0600   the `hub` tool — the `antasphere`
│                                      account CLI (its pre-extraction
│                                      location, kept for compatibility)
└── tools/                      0700
    └── <tool>.json             0600   every other tool, e.g. tools/slideless.json

Each file holds one tool's profiles. A tool file, e.g. tools/slideless.json:

{
  "activeProfile": "cloud",
  "profiles": {
    "cloud": {
      "baseUrl": "https://slideless.antasphere.com",
      "connectKeys": {
        "default": {
          "apiKey": "slk_…",
          "email": "[email protected]",
          "mintedAt": "2026-09-30T09:12:44.000Z"
        }
      }
    },
    "share.antasphere.com": {
      "baseUrl": "https://share.antasphere.com",
      "apiKey": "slk_…"
    },
    "localhost:3400": {
      "baseUrl": "http://localhost:3400",
      "apiKey": "slk_…"
    }
  }
}

baseUrl is the instance the profile is; a profile with no baseUrl is at the tool's cloud URL. apiKey is the tool's own login; connectKeys holds the keys the hub exchange minted, one per hub profile (the slot name, here default, is the hub profile's name).

The hub's config.json:

{
  "activeProfile": "default",
  "profiles": {
    "default": {
      "apiKey": "ant_…",
      "baseUrl": "https://account.antasphere.com",
      "email": "[email protected]",
      "workspaceId": "…"
    }
  }
}

workspaceId is the default organization at login — informational; the hub's server-side default is what applies. All config I/O is env-injected (no process.env reads) — tests point the whole config world at a temp dir via XDG_CONFIG_HOME.

Profiles

A profile is an instance, named for its host by default (cloud for the tool's cloud URL, share.antasphere.com, localhost:3400). selectProfile applies one rule, in this order:

  1. --profile <name> wins, and the profile must exist.
  2. --api-url, else the tool's URL env var, selects the profile of that instance; when several name it, the active one first, then cloud, then the first declared. When none does, a profile is made in memory, named for the host — cloud when it is the cloud URL — under a name no profile on disk has (a taken name falls back to the host, then <host>-2, <host>-3). The chassis passes apiUrl: firstSet(flags.apiUrl, env[URL_VAR]), the same merge resolveBaseUrl makes, so a blank flag never hides the env var.
  3. The active profile — the choice the person makes with <tool> use <profile>.
  4. The profile at the tool's cloud URL, cloud by default: the one on disk at that instance whatever its name (a profile named cloud that lives at another instance is not it), else made in memory under a free name — cloud, or the host (then <host>-2, …) when cloud is taken elsewhere.

created: true means the selected profile is not on disk yet (made in memory for an instance URL, or the implicit cloud profile) and its name is free. A profile with no baseUrl is at the cloud URL, for the selection and for the connect seam alike (the chassis hands connectOnDemand the same cloudUrl). Selection never writes; storeConnectKey is the only thing that writes such a profile, on first contact, and makes it active only when the tool had no profile at all. It never renames or repoints an existing profile, and never changes which profile is active once the tool has one.

An active profile pointing at an unreachable instance is still the selection — there is no silent fallback to cloud. The tool's error names the profile and says the hub login exists; that wording is the tool's. default is an ordinary profile name now, nothing pins it.

An illustration of what a tool can print for the three profiles above (a slideless profiles-style listing, not a command this package provides):

  PROFILE               INSTANCE                          SIGNS IN WITH
* cloud                 https://slideless.antasphere.com  hub
  share.antasphere.com  https://share.antasphere.com      key
  localhost:3400        http://localhost:3400             key

The last column is profileCredential: key (the profile's own apiKey, the tool's login), else hub (a key the exchange minted), else none.

Surface

  • configPath / loadConfig / saveConfig / clearConfig (env, tool) — the tool-namespaced store. HUB_TOOL names the hub namespace. redactKey renders keys safely for terminals. parseConfig(raw) is the tolerant parse (corrupt or foreign shapes → empty config); assertToolNamespace(tool) refuses anything but a lowercase slug.
  • resolveProfile(config, requested, { unknownHint }) — the named (or active) profile; an explicitly named missing profile throws CliUsageError.
  • selectProfile({ config, requested, apiUrl, cloudUrl, unknownHint }) — the selection rule of Profiles; returns { name, profile, source, created }, source being flag, url, active or cloud. Nothing is written, no profile is repointed.
  • CLOUD_PROFILE — 'cloud', the implicit profile at the tool's cloud URL.
  • normalizeUrl(url) / sameInstance(a, b) / profileNameFor(url, cloudUrl) — the comparable form of an instance URL (scheme, host with port, path; no trailing slash, query or fragment), equality on it, and the name an instance's profile gets (cloud, else the host with its port). profileUrl(profile, cloudUrl) is the instance a profile is at (its baseUrl, a blank one counting as none, else the cloud URL); firstSet(...values) the first non-blank value, trimmed — the one rule every resolver applies to flags, env vars and profile fields, the hub profile's apiKey and baseUrl included.
  • resolveBaseUrl({ flag, env, envVar, profile, cloudUrl, missingMessage }) — flag → tool env var → profile → cloud URL, trailing slashes stripped; a blank value counts as unset (SLIDELESS_URL="" never hides the cloud URL); throws missingMessage as a CliUsageError only when nothing resolves and the tool has no cloud URL.
  • resolveApiKey({ flag, env, envVar, profile }) — flag → env → profile, blank values skipped.
  • resolveOrg({ flag, env, envVar }) — the organization override for one command: --org, else the tool's org env var, else undefined (the hub's server-side default applies); blank values skipped. Nothing is stored; it never reaches the exchange.
  • CliUsageError(message, { exitCode }) — a usage-level error; runners print it and exit with exitCode (default 1).
  • CliAuthClient({ baseUrl, fetch? }) — the browserless email-OTP flow every instance speaks: request({ email }), complete({ email, otp, keyName?, expiresInDays?, workspaceId? }) (the full key appears exactly once), revoke(apiKey) (self-revocation = logout). Errors are CliAuthError with status/code/message.
  • CliIo + ask + printJson — the injectable I/O seam (env, streams, fetch, interactive prompt) so tool CLIs run in-process under test.
  • CliConnectClient({ hubBaseUrl, toolBaseUrl, fetch? }) — the cross-tool connect exchange (binding patterns §7, gcloud model — one hub login serves every federated tool): toolToken(hubApiKey, { resource }) asks the hub for a 120 s USER-scoped JWT plus the tool's one-time offline grant (hubRefreshToken; the exchange names NO org — hub ADR 014), cliConnect(token, hubRefreshToken?) trades the pair at the tool for a user-scoped tool-local key (no auth header — the JWT is the credential; the grant is relayed so a fresh user's connect is never refused hub_grant_missing), exchange(hubApiKey, body) composes both; the hub key never reaches the tool, the grant never reaches anything but the tool, and neither is ever persisted here. Errors are CliConnectError (a CliAuthError) tagged with the failing stage plus four remedies: needsHubLogin (hub 401, or 403 insufficient_scope from a pre-sso:exchange key), needsReExchange (tool 401 on an expired/replayed JWT), refusal (a 403 that is not the scope case, from either side — the account was refused, not the key) and exitCode (3 for a refusal, else 1).
  • lookupConnectKey / storeConnectKey / removeConnectKey / profileCredential — the cache of exchange-minted user-scoped keys, held on the profile of the instance they were minted for, in its connectKeys map (slot = the hub profile name; selectHubProfile(env) resolves it). ONE key per hub account serves every org — the org is never part of the credential or the slot. storeConnectKey(env, tool, profileName, hubProfileName, entry, { baseUrl }) writes baseUrl only on a profile that has none, a blank one counting as none (a profile is never repointed), creates a profile not on disk yet, and makes it active only when the tool had no profile at all. removeConnectKey never touches the tool's own apiKey. profileCredential(profile, hubProfileName?) answers key / hub / none. cachedConnectKey(profile, hubProfileName, hubEmail) is the slot read the seam serves from: the key only when both e-mails are there and match.
  • connectOnDemand({ tool, toolBaseUrl, cloudUrl?, hubCloudUrl?, profileName, env, resource?, fetch?, notify?, messages?, cached? }) — the seam a tool CLI runs when NO direct credential resolved. profileName is REQUIRED: the profile selectProfile chose; cloudUrl is what a profile with no baseUrl is at, the same value the selection got. The cache is read first, on that profile only when it is at this instance (a hit costs zero network); cached: false skips it. On a miss the anonymous GET /api/v1/instance probe gates the flow (a self-hosted or unreachable instance answers { outcome: 'not_cloud' } and the tool raises its classic no-key error), the hub credential is read from the HUB_TOOL profile, exchange runs with one re-exchange retry on a dead JWT (a missing or rejected hub login → a guided antasphere login CliUsageError, on either attempt; a refusal propagates as its CliConnectError), and the key is stored on the selected profile — never on one that is at another instance. The hub profile the exchange uses is the one ANTASPHERE_URL names (the profile at that instance, the way antasphere login picks it; hubCloudUrl lets that URL name the cloud slot, places a hub profile with no baseUrl at the hub and is the hub URL its exchange goes to; a profile's own baseUrl always wins), else the active one, and none when no hub profile is active (the account CLI would then take its cloud profile; a tool CLI asks for antasphere login). A cached tool key is served only when the hub profile's e-mail and the cached key's e-mail match, and never when either is missing. Success: { outcome: 'connected', key, cached, result?, stored? } — cached: true = served from the cache. notify receives the "Connected to …" notice on fresh exchanges (cache hits are silent); messages overrides the copy. probeToolInstance(toolBaseUrl, fetch?) is the discovery probe on its own; selectHubProfile(env, { cloudUrl? }) is that hub profile, whose name is the cache slot (HUB_URL_ENV is ANTASPHERE_URL; activeHubProfile(env) is kept, deprecated, as selectHubProfile(env)).
  • refreshConnectKey(opts) — the stranded-key recovery, same options as connectOnDemand. When a request answers 401 with a key served cached: true, the chassis calls it once: it evicts that key (only from a profile at this instance; a key of another instance is never touched) and exchanges again with the cache skipped. The chassis retries its request once with the new key, and a second 401 fails; not_cloud means nothing to recover with.
  • migrateLegacyConfig(env, { tool, legacyDir }) — one-shot, non-destructive import of a tool's pre-cli-core $XDG_CONFIG_HOME/<legacyDir>/config.json into the shared home. Imports only when the tool has no profiles in the shared home yet and the legacy file holds at least one; the legacy file is left in place.

Exit codes

  • 1 — a usage or wire error.
  • 3 — a refusal: a 403 at the exchange that is not the scope case; the message is the server's own sentence, printed as is.

Runners read exitCode off CliUsageError and CliConnectError.

Consuming it

"dependencies": { "@antasphere/cli-core": "^0.6.0" }

Registry tarballs ship a prebuilt dist/, no build script runs on install.

Release

Releases are tokenless via npm trusted publishing (OIDC): .github/workflows/publish.yml fires on a vX.Y.Z tag, verifies the tag matches package.json#version, runs lint/typecheck/test/build, and publishes with no npm token — npm authenticates the workflow's OIDC identity directly.

# bump package.json#version, commit, then:
git tag v0.6.0 && git push origin v0.6.0

The first publish was done by hand, and the trusted publisher is configured on npmjs.com; every release is the tag.

FieldValue
Organization or userantasphere
Repositorycli-core
Workflow filenamepublish.yml
Environment(leave empty)

Development

pnpm install
pnpm lint && pnpm typecheck && pnpm test && pnpm build

Branches per the workspace rule: prod (default, releases/tags) + dev (day-to-day work). License: MIT (permissive on purpose — this is plumbing consumed by public CLIs).

版本与发布记录

@antasphere/cli-core 共发布 6 个版本,首次发布于 2026/07/12;过去 12 个月发布了 6 个版本。

版本发布日期距今
0.6.0latest2026/10/1325 天前
0.5.02026/09/301 个月前
0.4.02026/07/163 个月前
0.3.02026/07/153 个月前
0.2.02026/07/133 个月前
0.1.02026/07/123 个月前

关于 @antasphere/cli-core 的常见问题

@antasphere/cli-core 是什么?

Shared CLI foundation for Antasphere tool CLIs: the ~/.config/antasphere config home, profiles that are instances (with the implicit cloud profile), the flag → env → profile → cloud URL resolution, the per-command organization override, the instance email

如何安装 @antasphere/cli-core?

在项目目录运行 npm install @antasphere/cli-core,或使用 pnpm add / yarn add / bun add。

@antasphere/cli-core 支持 TypeScript 吗?

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

@antasphere/cli-core 支持 ESM 还是 CommonJS?

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

@antasphere/cli-core 需要什么版本的 Node.js?

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

@antasphere/cli-core 可以商用吗?

@antasphere/cli-core 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@antasphere/cli-core 的下载量有多少?

根据 npm 官方下载统计,@antasphere/cli-core 在 2026/10/06 – 2026/11/04 这 30 天内被下载 2,755 次,日均约 92 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@antasphere/cli-core 还在维护吗?

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