@pithy-sh/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +72 -0
- package/scripts/templateManifest.ts +49 -0
- package/scripts/tsconfig.json +26 -0
- package/scripts/vendorTemplate.ts +84 -0
- package/scripts/verifyPack.ts +88 -0
- package/src/audit/cliAudit.ts +406 -0
- package/src/bin.ts +111 -0
- package/src/capabilities/add.ts +288 -0
- package/src/capabilities/addBootstrap.ts +275 -0
- package/src/capabilities/catalog.ts +175 -0
- package/src/capabilities/compose.ts +39 -0
- package/src/capabilities/configConstants.ts +74 -0
- package/src/capabilities/configImports.ts +397 -0
- package/src/capabilities/eject.ts +331 -0
- package/src/capabilities/emailProvisioner.ts +346 -0
- package/src/capabilities/entitlementGap.ts +70 -0
- package/src/capabilities/entryExports.ts +162 -0
- package/src/capabilities/flow.ts +550 -0
- package/src/capabilities/hostRegistry.ts +368 -0
- package/src/capabilities/loadFailure.ts +208 -0
- package/src/capabilities/manifests.ts +238 -0
- package/src/capabilities/mediaProvisioner.ts +471 -0
- package/src/capabilities/mintSecrets.ts +306 -0
- package/src/capabilities/paymentsProvisioner.ts +207 -0
- package/src/capabilities/prerequisites.ts +168 -0
- package/src/capabilities/r2Bucket.ts +113 -0
- package/src/capabilities/reconcile.ts +1483 -0
- package/src/capabilities/remove.ts +597 -0
- package/src/capabilities/requiredOptions.ts +92 -0
- package/src/capabilities/rotateSecrets.ts +305 -0
- package/src/capabilities/secrets.ts +178 -0
- package/src/capabilities/secretsDispatcher.ts +29 -0
- package/src/capabilities/secretsProvisioner.ts +389 -0
- package/src/capabilities/storageProvisioner.ts +414 -0
- package/src/capabilities/supportProvisioner.ts +515 -0
- package/src/capabilities/testersLoader.ts +52 -0
- package/src/capabilities/testersProvisioner.ts +236 -0
- package/src/capabilities/turnstileProvisioner.ts +347 -0
- package/src/capabilities/vectorProvisioner.ts +260 -0
- package/src/ci/fileModes.ts +223 -0
- package/src/ci/sourceFiles.ts +200 -0
- package/src/ci/workflowDrivers.ts +524 -0
- package/src/cloudflare/accountAnswer.ts +110 -0
- package/src/cloudflare/config.ts +685 -0
- package/src/cloudflare/storeId.ts +129 -0
- package/src/commands/add.ts +372 -0
- package/src/commands/alias.ts +205 -0
- package/src/commands/dashboard.ts +651 -0
- package/src/commands/deploy.ts +150 -0
- package/src/commands/dev.ts +37 -0
- package/src/commands/doctor.ts +2059 -0
- package/src/commands/email.ts +425 -0
- package/src/commands/env.ts +155 -0
- package/src/commands/feature.ts +359 -0
- package/src/commands/init.ts +538 -0
- package/src/commands/media.ts +303 -0
- package/src/commands/migrate.ts +129 -0
- package/src/commands/payments.ts +336 -0
- package/src/commands/provision.ts +368 -0
- package/src/commands/remove.ts +151 -0
- package/src/commands/secrets.ts +652 -0
- package/src/commands/seed.ts +229 -0
- package/src/commands/storage.ts +309 -0
- package/src/commands/support.ts +331 -0
- package/src/commands/testers.ts +1020 -0
- package/src/commands/token.ts +364 -0
- package/src/commands/turnstile.ts +271 -0
- package/src/commands/ui.ts +222 -0
- package/src/commands/upgrade.ts +517 -0
- package/src/commands/vector.ts +390 -0
- package/src/commands/worker.ts +295 -0
- package/src/dashboard/api.ts +323 -0
- package/src/dashboard/connect.ts +758 -0
- package/src/dashboard/contract.ts +289 -0
- package/src/dashboard/grant.ts +124 -0
- package/src/dashboard/registry.ts +519 -0
- package/src/dashboard/resolveTarget.ts +119 -0
- package/src/dev/delivery.ts +174 -0
- package/src/dev/devLogin.ts +155 -0
- package/src/dev/devLoginTargets.ts +91 -0
- package/src/dev/env.ts +206 -0
- package/src/dev/hostWorkers.ts +290 -0
- package/src/dev/keys.ts +111 -0
- package/src/dev/logging.ts +87 -0
- package/src/dev/openUrl.ts +75 -0
- package/src/dev/orchestrator.ts +1014 -0
- package/src/dev/ports.ts +220 -0
- package/src/dev/readyWatch.ts +142 -0
- package/src/dev/state.ts +90 -0
- package/src/devSecrets/bootstrapVars.ts +265 -0
- package/src/devSecrets/devVars.ts +240 -0
- package/src/devSecrets/edit.ts +256 -0
- package/src/devSecrets/file.ts +277 -0
- package/src/devSecrets/generate.ts +428 -0
- package/src/devSecrets/location.ts +80 -0
- package/src/devSecrets/mode.ts +71 -0
- package/src/devSecrets/records.ts +30 -0
- package/src/devSecrets/report.ts +99 -0
- package/src/devSecrets/seed.ts +344 -0
- package/src/devSecrets/store.ts +262 -0
- package/src/devSecrets/targets.ts +204 -0
- package/src/dispatch.ts +147 -0
- package/src/docs/catalog.ts +246 -0
- package/src/docs/writeCatalog.ts +45 -0
- package/src/doctor/cloudflare.ts +287 -0
- package/src/doctor/devPreferences.ts +155 -0
- package/src/doctor/devSecrets.ts +464 -0
- package/src/doctor/devVars.ts +414 -0
- package/src/doctor/devVarsLocal.ts +138 -0
- package/src/doctor/environments.ts +155 -0
- package/src/doctor/health.ts +354 -0
- package/src/doctor/localDelivery.ts +91 -0
- package/src/doctor/portsRegistry.ts +252 -0
- package/src/doctor/projectName.ts +584 -0
- package/src/doctor/secretBindings.ts +166 -0
- package/src/doctor/settings.ts +274 -0
- package/src/doctor/settingsSources.ts +202 -0
- package/src/doctor/workerName.ts +174 -0
- package/src/doctor/wranglerVars.ts +33 -0
- package/src/feature/bindings.ts +93 -0
- package/src/feature/create.ts +179 -0
- package/src/feature/destroy.ts +160 -0
- package/src/feature/devConfig.ts +201 -0
- package/src/feature/identity.ts +100 -0
- package/src/feature/manifest.ts +132 -0
- package/src/feature/ports.ts +615 -0
- package/src/feature/provision.ts +362 -0
- package/src/feature/sync.ts +148 -0
- package/src/feature/worktree.ts +282 -0
- package/src/help/groups.ts +47 -0
- package/src/help/rootUsage.ts +135 -0
- package/src/main.ts +73 -0
- package/src/migrations/ledger.ts +129 -0
- package/src/migrations/registry.ts +47 -0
- package/src/migrations/run.ts +1066 -0
- package/src/notifier/check.ts +129 -0
- package/src/notifier/installer.ts +48 -0
- package/src/notifier/notify.ts +152 -0
- package/src/notifier/state.ts +248 -0
- package/src/notifier/version.ts +59 -0
- package/src/platform/editor.ts +333 -0
- package/src/platform/rc.ts +118 -0
- package/src/platform/shell.ts +83 -0
- package/src/project/appBindings.ts +184 -0
- package/src/project/appWorkflows.ts +266 -0
- package/src/project/applyDomains.ts +166 -0
- package/src/project/askDomains.ts +220 -0
- package/src/project/atomic.ts +466 -0
- package/src/project/bindingEntries.ts +425 -0
- package/src/project/config.ts +701 -0
- package/src/project/dashboard.ts +118 -0
- package/src/project/deploy.ts +364 -0
- package/src/project/devVars.ts +113 -0
- package/src/project/domainPrompt.ts +191 -0
- package/src/project/domains.ts +386 -0
- package/src/project/envInventory.ts +356 -0
- package/src/project/environment.ts +125 -0
- package/src/project/extensions.ts +69 -0
- package/src/project/jsonc.ts +289 -0
- package/src/project/packageManager.ts +238 -0
- package/src/project/readOptionalFile.ts +342 -0
- package/src/project/rollback.ts +145 -0
- package/src/project/scaffold.ts +1088 -0
- package/src/project/templateFiles.ts +53 -0
- package/src/project/verifyDeploy.ts +230 -0
- package/src/project/versionMetadata.ts +77 -0
- package/src/project/workerAddress.ts +176 -0
- package/src/project/workerCommand.ts +564 -0
- package/src/project/workerIdentity.ts +50 -0
- package/src/project/workerManifest.ts +135 -0
- package/src/project/workerScaffold.ts +289 -0
- package/src/project/workerScope.ts +394 -0
- package/src/project/workers.ts +86 -0
- package/src/project/workflows.ts +281 -0
- package/src/project/wrangler.ts +168 -0
- package/src/provision/confirm.ts +86 -0
- package/src/provision/environment.ts +407 -0
- package/src/provision/featureConfig.ts +98 -0
- package/src/provision/mode.ts +62 -0
- package/src/provision/pendingSecrets.ts +96 -0
- package/src/provision/resources.ts +126 -0
- package/src/provision/secretBindings.ts +149 -0
- package/src/provision/store.ts +33 -0
- package/src/provision/unprovisioned.ts +114 -0
- package/src/provision/wranglerEnv.ts +220 -0
- package/src/rootFlags.ts +48 -0
- package/src/seed/drivers.ts +423 -0
- package/src/seed/media.ts +187 -0
- package/src/seed/plan.ts +137 -0
- package/src/seed/prepare.ts +224 -0
- package/src/seed/registry.ts +25 -0
- package/src/seed/run.ts +793 -0
- package/src/seed/safety.ts +206 -0
- package/src/terminal/logger.ts +42 -0
- package/src/terminal/output.ts +64 -0
- package/src/terminal/style.ts +132 -0
- package/src/test-utils/doctorHarness.ts +190 -0
- package/src/test-utils/migrateHarness.ts +126 -0
- package/src/test-utils/seedHarness.ts +173 -0
- package/src/test-utils/tempRepo.ts +45 -0
- package/src/tokens/config.ts +16 -0
- package/src/tokens/engine.ts +345 -0
- package/src/tokens/mintedTokens.ts +233 -0
- package/src/tokens/sinks.ts +84 -0
- package/src/ui/flow.ts +451 -0
- package/src/ui/react.ts +112 -0
- package/src/ui/routeAllowlist.ts +208 -0
- package/src/ui/scaffold.ts +113 -0
- package/src/ui/screenStyles.ts +127 -0
- package/src/ui/stubs.ts +135 -0
- package/src/ui/templates.ts +52 -0
- package/src/ui/wire.ts +311 -0
- package/src/ui/workerUi.ts +172 -0
- package/templates/starter/.dev.secrets.example.jsonc +43 -0
- package/templates/starter/.dev.vars.example +30 -0
- package/templates/starter/apps/api/package.json +22 -0
- package/templates/starter/apps/api/pithy.config.ts +65 -0
- package/templates/starter/apps/api/pithy.worker.jsonc +11 -0
- package/templates/starter/apps/api/src/bindings.workers.test.ts +18 -0
- package/templates/starter/apps/api/src/cloudflare-test.d.ts +11 -0
- package/templates/starter/apps/api/src/index.ts +8 -0
- package/templates/starter/apps/api/tsconfig.json +26 -0
- package/templates/starter/apps/api/wrangler.jsonc +68 -0
- package/templates/starter/biome.template.jsonc +75 -0
- package/templates/starter/gitignore +37 -0
- package/templates/starter/package.json +28 -0
- package/templates/starter/pithy.config.ts +67 -0
- package/templates/starter/plugins/no-console.grit +25 -0
- package/templates/starter/plugins/no-process-io.grit +25 -0
- package/templates/starter/tsconfig.json +14 -0
- package/templates/starter/tsconfig.tools.json +30 -0
- package/templates/starter/vitest.config.ts +124 -0
- package/templates/starter/vitest.workers.config.ts +26 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The built-in discovery catalog — the capabilities `pithy add --list` shows
|
|
6
|
+
* before anything is installed. Hand-maintained as capabilities ship; the full
|
|
7
|
+
* manifest is still read from the package after install (manifests.ts). Names and
|
|
8
|
+
* rationales track docs/CLI.md §4.2.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface CatalogEntry {
|
|
12
|
+
/** Capability name, the `pithy add <name>` argument. */
|
|
13
|
+
name: string;
|
|
14
|
+
/**
|
|
15
|
+
* The npm package providing it — `@pithy-sh/<name>` for all but one.
|
|
16
|
+
*
|
|
17
|
+
* `controlplane` ships inside `@pithy-sh/core`, because every capability contributing admin routes
|
|
18
|
+
* imports its guard, and a capability may depend on a core seam but never on a sibling package. This
|
|
19
|
+
* field is therefore the authority on where a capability's manifest lives, and `manifests.ts` resolves
|
|
20
|
+
* the directory from it rather than from the name — one place to state the exception, so the two
|
|
21
|
+
* cannot drift.
|
|
22
|
+
*/
|
|
23
|
+
package: string;
|
|
24
|
+
/** One-line rationale — why a project would enable this. */
|
|
25
|
+
whenToEnable: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The npm scope every capability ships under. */
|
|
29
|
+
export const CAPABILITY_SCOPE = "@pithy-sh";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The npm package a capability ships in — the catalog's `package`, or `@pithy-sh/<name>` when the
|
|
33
|
+
* catalog does not know it (an installed package that predates a catalog entry).
|
|
34
|
+
*
|
|
35
|
+
* **Use this rather than interpolating the name.** `@pithy-sh/${capability}` was the rule everywhere
|
|
36
|
+
* until `controlplane` shipped inside `@pithy-sh/core`, and every remaining hand-rolled interpolation
|
|
37
|
+
* is a place that silently reaches for a package that does not exist.
|
|
38
|
+
*/
|
|
39
|
+
export function capabilityPackageName(name: string): string {
|
|
40
|
+
return CATALOG.find((candidate) => candidate.name === name)?.package ?? `${CAPABILITY_SCOPE}/${name}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The package directory a capability's files live in — {@link capabilityPackageName} minus the scope.
|
|
45
|
+
* This is the `node_modules/@pithy-sh/<dir>` a manifest is read from.
|
|
46
|
+
*/
|
|
47
|
+
export function capabilityPackageDir(name: string): string {
|
|
48
|
+
return capabilityPackageName(name).slice(`${CAPABILITY_SCOPE}/`.length);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Packages that host more than the one capability named after them, and must therefore never be
|
|
53
|
+
* uninstalled when that capability is removed.
|
|
54
|
+
*
|
|
55
|
+
* `@pithy-sh/core` is every capability's dependency and the runtime the app is built on. Removing
|
|
56
|
+
* `controlplane` unwires the seam; uninstalling core would take the project with it.
|
|
57
|
+
*/
|
|
58
|
+
export function isSharedCapabilityPackage(pkg: string): boolean {
|
|
59
|
+
return pkg === `${CAPABILITY_SCOPE}/core`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const CATALOG: readonly CatalogEntry[] = [
|
|
63
|
+
{ name: "auth", package: "@pithy-sh/auth", whenToEnable: "Authentication and session management." },
|
|
64
|
+
{
|
|
65
|
+
name: "secrets",
|
|
66
|
+
package: "@pithy-sh/secrets",
|
|
67
|
+
whenToEnable: "Encrypted secret storage with a worker-only master key and automatic at-rest key rotation.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "email",
|
|
71
|
+
package: "@pithy-sh/email",
|
|
72
|
+
whenToEnable:
|
|
73
|
+
"Transactional and lifecycle email — magic link, OTP, welcome, alerts — sent as durable, tracked jobs.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "i18n",
|
|
77
|
+
package: "@pithy-sh/i18n",
|
|
78
|
+
whenToEnable:
|
|
79
|
+
"Enable it when your app serves readers in more than one language. It adds a translator seam every screen, error and email renders through, negotiates each reader's locale from the URL, their account, a cookie and Accept-Language, and formats dates, numbers and currency with Intl. Ships Spanish; add your own catalogs a sentence at a time.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "turnstile",
|
|
83
|
+
package: "@pithy-sh/turnstile",
|
|
84
|
+
whenToEnable:
|
|
85
|
+
"Stop bots at login, signup, and form-submit points with a Cloudflare Turnstile humanity check — stacked on any route, with test keys wired automatically in dev and staging.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "audit",
|
|
89
|
+
package: "@pithy-sh/audit",
|
|
90
|
+
whenToEnable:
|
|
91
|
+
"A queryable audit trail of security-relevant actions — who did what, when, and whether it succeeded — recorded from Workers and the CLI, attributed to the right actor.",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "storage",
|
|
95
|
+
package: "@pithy-sh/storage",
|
|
96
|
+
whenToEnable:
|
|
97
|
+
"Put your users' files in your own R2, behind one ObjectStore seam any capability can hold — multipart uploads for the large ones, Range, ETag, and Content-Disposition streaming for the rest, per-owner scoping, byte quotas, server-side copy, revocable share links, and a daily workflow that reconciles orphaned rows and orphaned objects in both directions. Uploaded bytes are treated as untrusted on the way out: nosniff and a locked-down CSP on every object response, and an active type (HTML, SVG, anything +xml, script) served as an attachment whatever was stored — so a user's file cannot execute on your origin. Keys are server-derived and opaque, so a client can never name an object or guess the one beside it; you supply a logical path, and that path is stored, indexed, and listable. It takes no position on what the bytes are — no transcoding, no thumbnails, no AI. That is @pithy-sh/media, which already presigns through this ObjectStore against its own bucket and credential name, and inherits none of these tables or routes for doing so. `pithy add storage` writes the bindings and touches no Cloudflare account; `pithy storage provision` creates the bucket. Objects belong to an authenticated owner, so add auth too.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "media",
|
|
101
|
+
package: "@pithy-sh/media",
|
|
102
|
+
whenToEnable:
|
|
103
|
+
"Store, track, and enrich media — images, video, audio, and documents — with direct-upload URLs and opt-in AI alt text, transcription, and text extraction. Config picks the backend; the package does the rest.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "leaderboard",
|
|
107
|
+
package: "@pithy-sh/leaderboard",
|
|
108
|
+
whenToEnable:
|
|
109
|
+
"Rank your players — daily, weekly, calendar-month, calendar-year, or all-time, because a board's window is a CRON expression rather than a fixed list. Boards are config. Closed windows stay in your own D1 for as long as you ask, in plain SQL you can join against your own tables. Writes are server-authoritative by default.",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "multiplayer",
|
|
113
|
+
package: "@pithy-sh/multiplayer",
|
|
114
|
+
whenToEnable:
|
|
115
|
+
"Authoritative, turn-based multiplayer sessions on Cloudflare — the server holds the game state no client can be trusted with, resolves it, and writes a durable result to your own D1. Games are pluggable: three example games ship — `battle` (simultaneous), `connect-n` (tic-tac-toe/Connect Four), `craps` (a wagering table) — each built on a reusable pattern helper you can layer your own game on, and you can register your own. Supports N players. Includes a wagering stack: provably-fair dice, persistent tables (buy in/cash out between rounds), and ledger-settled bets — pair with @pithy-sh/ledger. Pithy's first Durable Object: the CLI wires the DO binding and its class migration tag for you. Not rooms, chat, or real-time netcode — use Cloudflare's PartyServer for those. Sessions bind to an authenticated user, so add auth too.",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "rating",
|
|
119
|
+
package: "@pithy-sh/rating",
|
|
120
|
+
whenToEnable:
|
|
121
|
+
"Two numbers per player, per pool, in your own D1: a skill rating (MMR) that moves both ways with every result and feeds matchmaking, and an experience total (XP) that only ever rises and drives rank. They are separate on purpose — one number cannot both rank a player fairly and reward them for turning up. The algorithm is a choice rather than a hardcode: `elo` (1v1, transparent, one tunable), `glicko` (Glicko-2, 1v1, carrying an uncertainty term so a player returning on stale form is not rated as if they never left), and `trueskill` (any roster, and the only one that rates teams) ship built in, and registerRatingAlgorithm takes your own. Wiring a 1v1-only algorithm to a four-player game fails on deploy, not on the first recorded result. Pools are named, so several games can share one ladder or each can keep its own — and a pool is what @pithy-sh/matchmaking buckets its open queue on, which makes this the skill source that capability needs. Recording an outcome is server-authoritative by default and needs the rating:record scope, so a device cannot report that it won. This is the input system @pithy-sh/leaderboard deliberately left out: a leaderboard ranks what has happened, a rating estimates what will. Ratings bind to an authenticated player, so add auth too — without it every route is denied.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "matchmaking",
|
|
125
|
+
package: "@pithy-sh/matchmaking",
|
|
126
|
+
whenToEnable:
|
|
127
|
+
"How players find each other. @pithy-sh/multiplayer gives you an authoritative session once players are in it; this is the four ways they get there, and every one of them ends at a session id. A room code — short, shareable, short-lived, limited-use — for playing with someone who is already beside you. A direct invite by email or screen name, pending until the invitee accepts. A symmetric friend graph formed by mutual accept. And an open queue: one Durable Object per game pairs waiting players, bucketed by Cloudflare's own edge geolocation and by skill read from @pithy-sh/rating, widening each player's skill band the longer they wait until any opponent qualifies. A second Durable Object holds every online player's WebSocket and pushes match-found, invite-received, and friend-request in real time, over the Hibernation API so a connection waiting on nothing bills no duration. Two Durable Objects means two bindings and two class migration tags across every environment, and the CLI writes all of it — that wiring is the reason this is a capability rather than a snippet. Nothing here is a hard dependency: without auth every route is denied, without rating the queue buckets by region alone, without multiplayer session minting is off. Add auth at minimum, and read packages/matchmaking/docs/costs.md before production — a presence socket is cheap and a busy queue's alarm cadence is the dial that decides how cheap.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "ledger",
|
|
131
|
+
package: "@pithy-sh/ledger",
|
|
132
|
+
whenToEnable:
|
|
133
|
+
"Give every player a balance — a per-user ledger for chips, gold, gems, credits, or tokens, in your own D1. Every movement is atomic, idempotent (a payout delivered twice pays once), and overdraft-safe by a database CHECK constraint. Holds reserve a stake the moment a bet is placed, then release or capture it — which is what makes wagering safe, so it pairs with @pithy-sh/multiplayer. Currency-agnostic; whether the units map to money, and any regulation that implies, is yours. Reads scope to the caller; moving another player's balance needs the admin scope, so add auth too.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "vector",
|
|
137
|
+
package: "@pithy-sh/vector",
|
|
138
|
+
whenToEnable:
|
|
139
|
+
"Semantic search over your own Vectorize index, with the metadata half taken seriously. Vectorize returns partial results — silently — when you filter on a field whose metadata index was created after the vectors were written, and it stops at ten metadata indexes per index. Both fail quietly, in production. So filterable fields are a schema decision here: declare them once, and the package provisions the metadata indexes from that schema, types every filter against it, refuses a filter on a field the schema does not mark, and reconciles the live index against the config on every `pithy vector provision` — waiting for each metadata index to go live before anything writes a vector. `provision` records what it observed, and the Worker refuses to boot when the config declares a filterable field that record does not have, so a schema edited and deployed without re-provisioning fails naming the field rather than returning short results. Embeddings come from Workers AI, the model is pinned per index for writes and queries alike, and `pithy vector reprocess` re-embeds what a model change left behind. Documents live in your own D1; chunking stays yours. Pairs with @pithy-sh/media, whose extracted text and transcripts are the obvious thing to embed.",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "payments",
|
|
143
|
+
package: "@pithy-sh/payments",
|
|
144
|
+
whenToEnable:
|
|
145
|
+
"Four payment rails — Apple, Google, Stripe, Lemon Squeezy — resolving to one cross-rail entitlement, in your own Worker and your own D1. Buy Pro on iOS, be entitled on the web, with no hosted data plane holding your purchase history. Lemon Squeezy is the merchant of record on its rail, so global sales tax, EU VAT, invoicing and dunning are its problem rather than yours. A product is not an entitlement: `pro_monthly` and `pro_annual`, across four stores' catalogs, grant one key — `pro` — and gating code names the key, never a SKU. Every write converges on one idempotent projection keyed on (rail, provider transaction id), so a client submission, a provider webhook, and a reconciliation pass produce the identical row: a dropped client call costs nothing and a replayed webhook changes nothing. The projection is monotonic on the provider's own event time, because providers do not guarantee delivery order and a stale `expired` arriving after the `renewed` that superseded it would silently revoke a paying subscriber. Sandbox purchases are tracked as sandbox and never grant a production entitlement. A cron Workflow re-verifies what the webhooks missed, because webhook-only systems rot silently. Add auth — every route belongs to an authenticated purchaser or a machine proving authenticity, and there are no public routes. Add secrets: the four rails' credentials are read through it, so payments will not compose without it. Ledger fulfillment is opt-in per product; most products never touch a balance.",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "support",
|
|
149
|
+
package: "@pithy-sh/support",
|
|
150
|
+
whenToEnable:
|
|
151
|
+
"An inbound support inbox that lands mail in your own D1, classifies it on your own Workers AI binding, and links each sender to the account and purchases your app already knows about. Support is the employee a solo developer cannot hire, and every piece needed to build it was already in the catalog. Mail arrives through Cloudflare Email Routing, is parsed (multipart and attachments included), threaded on In-Reply-To and References so a conversation stays one conversation, and classified three ways at once \u2014 category, priority, sentiment \u2014 which makes a sortable inbox rather than a labeled one. The taxonomy is federated: eight categories ship, and you add your own with defineSupportCategories. Classification runs on your binding, so the inference lands on your bill and your customers' support mail never leaves your infrastructure. Everything is derived from immutable mail: a wrong classification is recomputed, not repaired, and there is no assignment and no status workflow, because that is a helpdesk product and this is not one. Replies go out through @pithy-sh/email carrying your domain, your DKIM, and correct threading headers \u2014 which only the Worker can set. The admin surface is control-plane only and default-denied. Cloudflare Email Routing takes over a zone's MX, so use a subdomain (support@help.yourdomain.com) and never your apex, or you will move your real mail off your existing provider.",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "testers",
|
|
155
|
+
package: "@pithy-sh/testers",
|
|
156
|
+
whenToEnable:
|
|
157
|
+
"Google Play makes every new personal developer account run a closed test with at least 12 testers opted in for 14 continuous days before it will grant production access, and there is no screen anywhere that tells you where you stand. Lose one tester on day nine and the clock effectively starts again. This runs that process: a cohort with a target and a window, a roster you invite by email from your own domain, a one-tap confirmation link that needs no account, and a fourteen-day clock replayed from your own event log so a correction is a recomputation rather than an overwrite. The clock is honest about what it is. Pithy's count is an estimate from your own invite records; Google's count is authoritative and no API exposes it — not the roster, not the number, not a tester quietly opting out — so every figure derived from it is named `estimated`, the response carries that sentence as a required field, and nothing here claims to read the console. What Pithy can read is activity, and that is the part nobody else has: resolve each tester to their user through `@pithy-sh/auth` and you learn who has actually opened the app, so a tester dark for eight days shows up on day eight rather than on day fourteen when the count finally moves. A daily Workflow advances state, chases whoever needs chasing under a mandatory per-tester cooldown, and writes one snapshot per cohort per day — which is what makes the trend chartable at all, because activity cannot be reconstructed after the fact the way an event log can. Every nudge ships default copy, so this works from the CLI with no dashboard; a control-plane caller may override the words as plain text and never as markup, because those words go out over your own DKIM signature.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "controlplane",
|
|
161
|
+
package: "@pithy-sh/core",
|
|
162
|
+
whenToEnable:
|
|
163
|
+
"Let a management client — the Pithy dashboard, or one you write yourself — reach into your own Worker, with no data plane in between. Present and denying by default: with no connection registered every route answers 403, and there is no backdoor to open. `pithy dashboard connect --env prod` grants exactly the operations you name, per environment, so a staging credential can never touch production. The credential is asymmetric — the client holds a private key, you hold and can revoke the public one — so nothing secret of yours ever leaves your infrastructure and a breach on their side is not a breach on yours. Rotation is append, prove, then expire, never replace, so a rotation that fails leaves the old key working instead of locking anyone out; revocation is a row you delete, immediate and needing nothing from them. Every call carries a signed 60-second single-scope token bound to a digest of its own body, is checked for replay, and lands in your audit trail under its own actor kind, so what the dashboard did is answerable separately from what your users did. Capabilities contribute their own admin routes behind it — payments puts manual entitlement grant and revoke here — so adding one adds its management surface with nothing to wire. It ships inside @pithy-sh/core, so it is already installed; adding it composes it, and connecting a client is the deliberate second step.",
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
/** A catalog entry tagged with whether the project already has it installed. */
|
|
168
|
+
export interface CatalogListing extends CatalogEntry {
|
|
169
|
+
installed: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Tag every catalog entry with its installed state, in catalog order. */
|
|
173
|
+
export function buildCatalogListing(installed: ReadonlySet<string>): CatalogListing[] {
|
|
174
|
+
return CATALOG.map((entry) => ({ ...entry, installed: installed.has(entry.name) }));
|
|
175
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { Capability } from "@pithy-sh/core/src/capability/capability";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* **Assemble a set of capabilities the way a Worker does at startup.**
|
|
8
|
+
*
|
|
9
|
+
* `createBackend` runs exactly this loop (`@pithy-sh/core/src/createBackend.ts`) — every hook, in
|
|
10
|
+
* composition order — and until a capability's hook has run, half of what that capability reports is
|
|
11
|
+
* a placeholder rather than an answer. `@pithy-sh/i18n` fills its `composedMessages` in its hook, so
|
|
12
|
+
* its `layersFor` walks empty catalogs before it; `@pithy-sh/email` adopts that `layersFor` in its
|
|
13
|
+
* own, so `hostCatalogs()` is `{}` and every `email/*` key resolves to its raw key string.
|
|
14
|
+
*
|
|
15
|
+
* That last sentence is the whole reason this is a shared function rather than a loop each caller
|
|
16
|
+
* writes. **Reading an uncomposed capability is silent and it is worse than not reading it at all**:
|
|
17
|
+
* `enqueueEmail` falls back to the kit's own English when it is handed no `layersFor`, so a project
|
|
18
|
+
* that adds `i18n()` and gets an uncomposed one mails a footer link labeled
|
|
19
|
+
* `email/shell.unsubscribe` — adding the capability broke a path that was correct without it. Three
|
|
20
|
+
* CLI sites read one of those values, each was written independently, and two of the three forgot.
|
|
21
|
+
*
|
|
22
|
+
* ## Why the CLI composes the project-wide union rather than one Worker's set
|
|
23
|
+
*
|
|
24
|
+
* `createBackend` composes exactly the capabilities of the Worker it is booting. Every caller here is
|
|
25
|
+
* resolving a **host Worker that composes nothing at all** — `pithy email provision`'s standalone send
|
|
26
|
+
* host, `pithy testers`' daily-pass host, `pithy dev`'s local materialization of both. One host serves
|
|
27
|
+
* the whole project (its name carries `<project>-<env>-`, not a Worker), and it is deployed once
|
|
28
|
+
* whichever Worker's `pithy.config.ts` brought the capability in. So the set it is stamped from is the
|
|
29
|
+
* project's whole capability surface, and a project where one Worker composes `i18n()` and another
|
|
30
|
+
* composes `email()` gets a host carrying the catalogs — which is the honest answer for a Worker that
|
|
31
|
+
* belongs to neither of them.
|
|
32
|
+
*
|
|
33
|
+
* Composition is idempotent: every kit hook assigns rather than accumulates, so composing a set twice
|
|
34
|
+
* leaves it where composing it once did.
|
|
35
|
+
*/
|
|
36
|
+
export function composeCapabilities(capabilities: Capability[]): Capability[] {
|
|
37
|
+
for (const capability of capabilities) capability.compose?.({ capabilities });
|
|
38
|
+
return capabilities;
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CONFIG_CONSTANTS,
|
|
6
|
+
type ConfigConstant,
|
|
7
|
+
type ConfigConstantRef,
|
|
8
|
+
type ConfigOption,
|
|
9
|
+
type ConfigOptionValue,
|
|
10
|
+
} from "@pithy-sh/core/src/capability/manifest";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Which value a capability's option is written with: the adopter's override, the scaffold's constant, or
|
|
14
|
+
* the manifest's literal default — decided once, for `pithy add` and `pithy upgrade` alike.
|
|
15
|
+
*
|
|
16
|
+
* **The rule this exists for: no capability asks an adopter to write down an origin.** Every one that
|
|
17
|
+
* did got production's origin written into staging, silently and separately — `auth.baseURL` mailed
|
|
18
|
+
* staging's testers magic links into production, `email.baseUrl` unsubscribed them there, and the Stripe
|
|
19
|
+
* return URLs landed a staging payer on a production account that had bought nothing. Three capabilities,
|
|
20
|
+
* one mistake, three discoveries on three different days, which is the shape that says the fix does not
|
|
21
|
+
* belong inside any one capability (#256).
|
|
22
|
+
*
|
|
23
|
+
* So an option whose value is an origin names `publicOrigin` in its manifest and the writers render
|
|
24
|
+
* `PUBLIC_ORIGIN` — the constant `pithy init` scaffolds, `originFor(compositionEnvironment(), DOMAINS)`,
|
|
25
|
+
* which follows the environment the Worker is composing in and falls back to the local origin rather than
|
|
26
|
+
* to another environment's.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The declaration a scaffolded config makes: `export const PUBLIC_ORIGIN = …`, at the top level.
|
|
31
|
+
*
|
|
32
|
+
* `export` is optional in the pattern and present in the scaffold — the constant is exported so an
|
|
33
|
+
* adopter's own code can build a link against the same origin, and so that a config composing no
|
|
34
|
+
* capability that takes one does not fail its own `biome check` on an unused variable.
|
|
35
|
+
*/
|
|
36
|
+
function declarationPattern(constant: ConfigConstant): RegExp {
|
|
37
|
+
return new RegExp(`^\\s*(?:export\\s+)?(?:const|let|var)\\s+${CONFIG_CONSTANTS[constant]}\\b`, "m");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether this Worker's `pithy.config.ts` actually declares the constant.
|
|
42
|
+
*
|
|
43
|
+
* Checked rather than assumed, because a project scaffolded before the constant existed does not have it
|
|
44
|
+
* — and writing `baseURL: PUBLIC_ORIGIN` into a config with no such identifier trades a hardcoded origin
|
|
45
|
+
* for a config that does not compile. The literal default is what those projects keep getting, and
|
|
46
|
+
* `pithy upgrade` starts writing the constant the day the adopter adds it.
|
|
47
|
+
*
|
|
48
|
+
* Line-anchored, and on a declaration keyword rather than the bare name: the identifier appears in every
|
|
49
|
+
* option line this writer has already written, and a substring match would read a *use* as a
|
|
50
|
+
* declaration and hand the next capability a name still not defined anywhere.
|
|
51
|
+
*/
|
|
52
|
+
export function declaresConstant(source: string, constant: ConfigConstant): boolean {
|
|
53
|
+
return declarationPattern(constant).test(source);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What to render for one option, given the config it is being written into.
|
|
58
|
+
*
|
|
59
|
+
* **An override always wins.** `--set baseURL=https://…` and a prompt answer are an adopter saying they
|
|
60
|
+
* want a literal, and that is a legitimate thing to want — a Worker fronted by something Pithy does not
|
|
61
|
+
* know about has an origin no derivation can produce. What is not legitimate is *defaulting* to one.
|
|
62
|
+
*/
|
|
63
|
+
export function optionValue(
|
|
64
|
+
option: ConfigOption,
|
|
65
|
+
source: string,
|
|
66
|
+
override: ConfigOptionValue | undefined,
|
|
67
|
+
): ConfigOptionValue | ConfigConstantRef | undefined {
|
|
68
|
+
if (override !== undefined) return override;
|
|
69
|
+
if (option.constant && declaresConstant(source, option.constant)) return { constant: option.constant };
|
|
70
|
+
// `undefined` when the option is required and this run named no value — the one thing this function
|
|
71
|
+
// will not do is invent one. It is the caller that knows which capability is being written, so the
|
|
72
|
+
// caller raises the refusal that names the flag; see `requiredOptions.ts`.
|
|
73
|
+
return option.default;
|
|
74
|
+
}
|