@pplethai/cli
v0.5.1
Published
The Builder's terminal tool: three commands between a PPLE ID and a live app.
Readme
pple CLI
The Builder's terminal tool: three commands between a PPLE ID and a live app.
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 npmGit 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 platform Skill overlay the Builder's AI coding
agent can load (ADR-0001, ADR-0006). The App Slug stays platform-generated;
bare pple init in a folder that already has an App Link is refused.
pple deploy refreshes that same Skill overlay, 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 Skills.
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
Skills and the golden-path template move forward (ADR-0006, ADR-0007). 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 machineLayout
| File | What it holds |
| ---- | ------------- |
| src/cli.ts | argument parsing and the command table |
| src/commands/ | one file per command, each taking only Deps |
| src/deps.ts | every seam: fetch, clock, subprocesses, output, template/skills dirs |
| src/device-flow.ts | RFC 8628 against PPLE ID |
| src/api.ts | the Provisioner API, and the only place that knows its error codes |
| src/git.ts | git, and the only place that names it or holds the repo token |
| src/scaffold.ts | copying the golden-path template |
| src/skills.ts | refreshing the platform Skill overlay into an app folder |
| src/npm.ts | npm: the registry's latest version, and the install channel |
| src/update.ts | the daily update check, its cadence state, and its opt-outs |
| askpass/pple-askpass.mjs | what 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 and Skills, then esbuild → dist/bin.jsTests 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 copies of the golden-path template
(npm run build:template copies ../template) and platform Skills
(npm run build:skills copies ../skills/pple-platform). Running from a
checkout falls back to those repo paths directly. PPLE_TEMPLATE_DIR,
PPLE_SKILLS_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 nothingBuilders install with npm install -g @pplethai/cli.
