@pplethai/cli

中文文档
0.6.0发布于 2026/11/02ES Module零依赖

The Builder's terminal tool: three commands between a PPLE ID and a live app, and everything else a Builder can do in the Console.

原始使用文档
文档目录9 节
  1. Builder Parity
  2. Asking for help is safe
  3. Consequential actions
  4. Reaching the app's code
  5. How the commands work
  6. Staying current
  7. Layout
  8. Development
  9. CLI release

pple CLI

The Builder's terminal tool: three commands between a PPLE ID and a live app, and everything else a Builder can do in the Console.

pple auth login                 # sign in with your PPLE ID (browser, no password in the terminal)
pple init                       # create an app (asks for App Name) and set up its code
pple init --name "Sign-up"      # same, without a prompt (for agents/scripts)
pple init brave-otter           # set up an app that already exists on the platform
pple deploy -m "Add the form"   # send the changes and put them live
pple upgrade                    # update pple itself from npm
pple help                       # every command; `pple <command> help` explains one

Builder Parity

Anything a Builder can view or change in the Console can be viewed or changed here (ADR-0020). Platform Admin work (the approval queue, the all-apps index, direct edits, Move owner, running a Code Review, admin Retire / Purge / Delete) stays in the Console, and the CLI has no command for it.

AreaCommands
Accountauth login, auth logout, whoami, agreement
Appsapps, init, info, rename, retire, retry-setup
Deploysdeploy, redeploy, deploys [<id>] (reads a failed deploy's log)
Configenv list, env set, env delete
Publicationpromote, invites [add|remove], roles, request publish|change|limit|retire, requests
Peoplecollaborators [add|remove]
Notes and reviewsnotes [add], reviews [<id>]

Every command that works on one app takes --app <slug> and otherwise uses the App Link of the folder it runs in. Read commands take --json and print the platform's own payload; Thai text is the default. pple invites and pple collaborators print full phone numbers, as the Console does for an Owner.

A CI test keeps this true. test/builder-parity.test.ts reads the Console's api object (console/web/console.ts) and fails when a method that isn't Platform Admin work has neither a command that names it in covers (src/registry.ts) nor an entry with a reason in the test's EXEMPT list. It proves a command exists, not that it behaves the same; permissions stay aligned because only the Provisioner enforces them. CLI CI runs whenever console/web/console.ts changes. A merged command reaches Builders only in a CLI release (ADR-0008), so the published CLI can briefly lag the Console.

Asking for help is safe

pple help, pple <command> help, pple <command> <subcommand> help, --help and -h explain a command and nothing else: they never reach the network, change anything, ask a question, need a sign-in, or run the daily update check. help counts only directly after the command's own words, so pple info --app help still means an app with that slug and pple env set API_URL help still sets the value help; --help and -h win anywhere. Agents explore with --help, so this is checked for every command in test/help.test.ts.

Consequential actions

At a terminal these ask a person, as the Console does. Run with nobody there (an agent, a script, a pipe) each one refuses unless given the specific value being confirmed, so the target is named in the Builder's transcript. There is no generic --yes.

ActionUnattended runs must pass
pple retire--confirm <slug>
pple collaborators add <phone>the lookup's token, on a second run: pple collaborators add --confirm <token>
pple request publish --address <name>--confirm <name>
pple agreementnothing works: only a person at a terminal, or the Console, can accept

The Skill tells agents to get the Builder's OK before passing any of these.

Git is an implementation detail, and so is GitHub. A Builder is told what happened to their app — "Sending your changes…", "Volunteer Sign-up is live at https://brave-otter.ppleth.ai" — never what happened to a branch, and never that there is an account somewhere they should go and get. Only a genuine conflict surfaces the tools underneath, and even then with an instruction rather than a stack trace.

Reaching the app's code

App repos are private and the Builder's machine has no credential for them. Before each command that touches code, the CLI asks the Provisioner for a token scoped to that one repo, keeps it in memory, and hands it to git through the child process's environment: GIT_ASKPASS points at askpass/pple-askpass.mjs, which reads the token back out of that environment when git asks for a password. origin stays a plain URL, so the token never reaches argv (world-readable via ps), .git/config, or any other file. Commits are authored as the Builder's PPLE ID identity, which the same response supplies — the pusher is the platform's GitHub App, and would otherwise be the only name in an app's history. The reasoning is ADR-0005.

How the commands work

pple auth login runs the OAuth device authorization grant against PPLE ID: the CLI prints a code, opens the verification page, and polls until the Builder approves it. Tokens land in $XDG_CONFIG_HOME/pple/credentials.json (mode 0600) — never inside an app folder, which is a repo the platform owns. pple whoami renews an expiring token silently via the refresh token.

pple init creates a Builder App (prompt for App Name, or --name) via the same Provisioner POST /apps the Console uses, waits until the app is ready, then scaffolds it — or, given an App Slug, scaffolds an app that already exists. Either way it fetches the app's code, copies the golden-path template under the app's own name, writes .pple/app.json linking the folder to the app, and installs the Skill and Platform Rules the Builder's AI coding agent reads (ADR-0001, ADR-0019). The App Slug stays platform-generated; bare pple init in a folder that already has an App Link is refused.

pple deploy records everything in the folder, sends it to the app's repo — merging in a Collaborator's work if there is any — then watches the platform build it and reports the live URL, or the build output that explains why nothing changed. It needs no arguments: the link file says which app. Account-only commands (auth login / logout / whoami) never touch the Skill or the Platform Rules.

Skill and Platform Rules are not in this package. pple init, deploy, info and env, run in an app's folder, ask the platform for the current hash of both and download one whole version only when it differs from the copy on disk, so a merge to main reaches Builders on their next command with no CLI release (ADR-0019). The copy is written once per agent, in the format it reads, and every path is git-ignored:

AgentSkillPlatform Rules
Claude Code.claude/skills/pple-platform/.claude/rules/pple-platform.md
Cursor.agents/skills/pple-platform/.cursor/rules/pple-platform.mdc (alwaysApply)
Codex.agents/skills/pple-platform/.pple/rules.md, by the one-line pointer in AGENTS.md
Gemini CLI.agents/skills/pple-platform/none

Which folder is refreshed is the one the command was run in, never the app --app names; outside an app folder nothing happens, and asking for help never refreshes. A refresh says something only when files changed (first install, a location put back, a new version), and says so on stderr, so --json output stays parseable. A refresh that fails warns, keeps the last copy and never blocks the command; each location is replaced by renaming a finished copy over the old one, so a download that dies halfway leaves nothing half-written. Sibling skills, other rule files and an AGENTS.md the Builder wrote are never touched: the pointer is created only when the file is missing. The version the platform sends is untrusted — a file path that would leave its folder refuses the whole version.

pple apps, rename, retire, retry-setup, redeploy, deploys, promote, invites, request, requests, roles, collaborators, notes, reviews are the Console's buttons from the terminal; each calls the same Provisioner route the Console does. request publish and request change upload an icon from a file path (--icon) the way the Console's picker does, and name roles by key or Thai label, resolved against the Provisioner's GET /role-options (pple roles) — the list the Console's picker shows.

pple upgrade asks npm what the latest @pplethai/cli is and, when this copy is one npm's global root holds, installs it — which is also how the golden-path template moves forward (ADR-0007; the Skill no longer travels with the CLI, ADR-0019). Any other install channel is told the manual npm install -g @pplethai/cli line rather than having a second copy installed over the first, and so is a failed install, carrying npm's own reason. The CLI never uses sudo: a global folder the Builder can't write to is theirs to fix, not the CLI's to force.

Staying current

About once a day, before running the command it was given, the CLI asks npm whether there is a newer pple. It runs before the command because that is the only moment an upgrade is free: nothing is half-done, so the new version can simply be handed the same command line. At a terminal the Builder is asked, and on a yes the CLI installs the new version and re-runs the original command on it. With nobody at the keyboard — the AFK agent runs the CLI is built to serve — it prints one line about the new version and gets on with the command; it never asks a question that could hang.

The check is written to fail towards the command. A registry that can't be reached says nothing at all and costs one request, not one per command: update-check.json in the CLI's config directory records that npm was asked, not what it answered. A failed upgrade prints npm's own reason and the manual line, then offers to carry on with the current CLI (unattended: carries on). Two ways to turn the whole thing off:

PPLE_NO_UPDATE_CHECK=1 pple deploy          # this run, or exported in CI
echo '{ "updateCheck": false }' > ~/.config/pple/config.json   # this machine

Layout

FileWhat it holds
src/registry.tsthe command table: every command, declared once with its help and what it covers; resolves a command line
src/command.tswhat a command is, and parsing its options
src/help.tsthe text of pple help, built from the table
src/cli.tsruns one command line: help first, then the update check, then the command
src/target.tswhich app a command acts on (--app, else the App Link)
src/commands/the commands, each taking Deps and what it was given
src/labels.ts, src/requests.tsthe Thai a Builder reads for platform values and Requests
src/deps.tsevery seam: fetch, clock, subprocesses, output, template dir
src/device-flow.tsRFC 8628 against PPLE ID
src/api.tsthe Provisioner API, and the only place that knows its error codes
src/git.tsgit, and the only place that names it or holds the repo token
src/scaffold.tscopying the golden-path template
src/guidance.tsrefreshing the Skill and Platform Rules into an app folder
src/npm.tsnpm: the registry's latest version, and the install channel
src/update.tsthe daily update check, its cadence state, and its opt-outs
askpass/pple-askpass.mjswhat git runs to be told the app's repo token

Development

npm install
npm test            # drives whole commands against a fake Provisioner and real git
npm run typecheck
npm run build       # bundles the template, then esbuild → dist/bin.js

Tests run the real command path — real git in a temp directory, a fake platform behind Deps.fetch, and a virtual clock so polling loops finish instantly. test/support/harness.ts is the entry point.

App repos are served in tests by git http-backend behind Basic auth (test/support/git-server.ts), accepting only tokens the fake platform minted. A local path would let every clone and push succeed with no credential at all, and nothing would show that the platform's token is what opens a private repo.

The published package carries its own copy of the golden-path template (npm run build:template copies ../template). Running from a checkout falls back to that repo path directly. The Skill is not bundled: tests serve it from the fake platform. PPLE_TEMPLATE_DIR, PPLE_API_BASE, PPLE_SSO_BASE, PPLE_CLIENT_ID, PPLE_REPO_BASE, and PPLE_REGISTRY_BASE override the defaults for staging work.

CLI release

A full Phase 1 launch includes a CLI release: shipping @pplethai/cli (bin: pple) to the npm latest dist-tag, which is what Builders install and self-update from. This is not part of the Cloudflare Worker deploy — a Worker rollback must never ship or unship a CLI version.

Releases happen through the CLI release workflow (.github/workflows/cli-release.yml): anyone with repository write dispatches it on main, and it re-runs typecheck, test, and build, asks the release gate whether the version in package.json may reach the registry, publishes, and only then tags the commit cli-vX.Y.Z. The one exception is the bootstrap publish that creates the package on npm, which a human runs once — see docs/deploy-phase1.md §3 for both.

The version comes from package.json on main — the workflow takes no version input, and refuses to release a version npm already holds. Nobody edits that version by hand: a PR changing what this package ships carries a Changeset (npx changeset at the repo root, or a skip-changeset label if it ships nothing), and the Version Packages PR spends those Changesets into package.json and CHANGELOG.md. So main may sit ahead of npm — a version here is prepared, not shipped. See .changeset/README.md and ADR-0008.

Past the bootstrap publish that creates the package, there is no supported way to release from a laptop. The release gate is scripts/release-gate.mjs, and it answers locally too:

node scripts/release-gate.mjs release   # may this version reach npm?
npm publish --access public --dry-run   # inspect the tarball, upload nothing

Builders install with npm install -g @pplethai/cli.

版本与发布记录

@pplethai/cli 共发布 12 个版本,首次发布于 2026/07/20;过去 12 个月发布了 12 个版本。

版本发布日期距今
0.6.0latest2026/11/021 天前
0.5.12026/08/202 个月前
0.5.02026/08/142 个月前
0.4.22026/07/243 个月前
0.4.12026/07/243 个月前
0.4.02026/07/213 个月前

关于 @pplethai/cli 的常见问题

@pplethai/cli 是什么?

Builder的终端工具:在ple ID和活动应用程序之间的三个命令,以及Builder可以在控制台中执行的所有其他操作。

如何安装 @pplethai/cli?

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

@pplethai/cli 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/pplethai__cli 是否存在。

@pplethai/cli 支持 ESM 还是 CommonJS?

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

@pplethai/cli 需要什么版本的 Node.js?

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

@pplethai/cli 可以商用吗?

@pplethai/cli 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@pplethai/cli 还在维护吗?

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