@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,135 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fromZodError, InternalError } from "@pithy-sh/core/src/error/pithyError";
|
|
7
|
+
import { parse } from "comment-json";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `pithy.worker.jsonc` — the per-worker manifest that sits beside `wrangler.jsonc` in every `apps/<name>/`.
|
|
12
|
+
* It is **ours**; `wrangler.jsonc` stays wrangler's. Discovery keys on this file, not `wrangler.jsonc`, so a
|
|
13
|
+
* non-Worker process (a Vite frontend with no `wrangler.jsonc` at all) can still join the dev set — it just
|
|
14
|
+
* carries a `dev.command` instead. The file is JSONC (comment-documented), matching the JSONC-everywhere rule.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** wrangler's own "ready" banner — the fallback `readySignal` when a worker declares none. */
|
|
18
|
+
export const DEFAULT_READY_SIGNAL = "Ready on https?://";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The placeholder `pithy dev` replaces with the worker's pinned port in every element of a `dev.command`.
|
|
22
|
+
* A dev server needs the port on its argv, and `pithy dev` spawns with **no shell** — `$WEB_PORT` in an
|
|
23
|
+
* argv array is a literal, never an expansion. So the manifest writes the token and dev substitutes it.
|
|
24
|
+
*/
|
|
25
|
+
export const DEV_PORT_TOKEN = "{port}";
|
|
26
|
+
|
|
27
|
+
/** How `pithy dev` runs one worker locally: whether it autostarts, what marks it ready, its port, its command. */
|
|
28
|
+
export const WorkerDev = z
|
|
29
|
+
.object({
|
|
30
|
+
autostart: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.default(false)
|
|
33
|
+
.describe(
|
|
34
|
+
"Must this worker run for the local dev environment to function? pithy dev starts exactly the autostart workers.",
|
|
35
|
+
),
|
|
36
|
+
readySignal: z
|
|
37
|
+
.string()
|
|
38
|
+
.default(DEFAULT_READY_SIGNAL)
|
|
39
|
+
.describe("Regex source matched against this process's output to mark it ready. Defaults to wrangler's banner."),
|
|
40
|
+
preferredPort: z
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.positive()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe(
|
|
46
|
+
"Preferred local port — a hint only. The feature's reserved block in .dev.config.json is authoritative.",
|
|
47
|
+
),
|
|
48
|
+
command: z
|
|
49
|
+
.array(z.string().min(1))
|
|
50
|
+
.min(1)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe(
|
|
53
|
+
`Command that starts this process instead of \`wrangler dev\`. Set it for a non-Worker process, e.g. a Vite frontend with no wrangler.jsonc. The token ${DEV_PORT_TOKEN} in any element is replaced with this worker's pinned port.`,
|
|
54
|
+
),
|
|
55
|
+
})
|
|
56
|
+
.describe("Local dev-orchestration settings for one worker (the `dev` block of pithy.worker.jsonc).");
|
|
57
|
+
export type WorkerDev = z.output<typeof WorkerDev>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* How a worker's front end is built. Present only when `pithy ui add` scaffolded one: the SPA is served
|
|
61
|
+
* by the Worker it lives in, so its assets must exist before `wrangler deploy` uploads them — `pithy deploy`
|
|
62
|
+
* runs `build` through the project's package manager, in the worker's own directory, first.
|
|
63
|
+
*/
|
|
64
|
+
export const WorkerUi = z
|
|
65
|
+
.object({
|
|
66
|
+
stub: z
|
|
67
|
+
.string()
|
|
68
|
+
.min(1)
|
|
69
|
+
.describe("The framework stub this worker's front end was scaffolded from, e.g. react. Upgrades read it."),
|
|
70
|
+
build: z
|
|
71
|
+
.array(z.string().min(1))
|
|
72
|
+
.min(1)
|
|
73
|
+
.describe(
|
|
74
|
+
'Argv that builds the front end, run through the project\'s package manager from this worker\'s directory before wrangler deploy, e.g. ["vite", "build"].',
|
|
75
|
+
),
|
|
76
|
+
})
|
|
77
|
+
.describe("The front end this worker serves (the `ui` block of pithy.worker.jsonc): its stub and its build.");
|
|
78
|
+
export type WorkerUi = z.output<typeof WorkerUi>;
|
|
79
|
+
|
|
80
|
+
/** The `pithy.worker.jsonc` document: the dev-set descriptor beside a worker's `wrangler.jsonc`. */
|
|
81
|
+
export const WorkerManifest = z
|
|
82
|
+
.object({
|
|
83
|
+
// A resolved default (not `{}`): an outer `.default({})` returns the literal, bypassing WorkerDev's own
|
|
84
|
+
// field defaults, so a manifest omitting `dev` would get an empty block instead of the wrangler defaults.
|
|
85
|
+
dev: WorkerDev.default({ autostart: false, readySignal: DEFAULT_READY_SIGNAL }).describe(
|
|
86
|
+
"Local dev-orchestration block for this worker.",
|
|
87
|
+
),
|
|
88
|
+
ui: WorkerUi.optional().describe(
|
|
89
|
+
"The front end this worker serves. Absent for an API-only worker; written by pithy ui add.",
|
|
90
|
+
),
|
|
91
|
+
})
|
|
92
|
+
.describe("Pithy's per-worker manifest (pithy.worker.jsonc): how a worker joins the local dev set.");
|
|
93
|
+
export type WorkerManifest = z.output<typeof WorkerManifest>;
|
|
94
|
+
|
|
95
|
+
/** The manifest filename discovery and scaffolding both key on. */
|
|
96
|
+
export const WORKER_MANIFEST_FILE = "pithy.worker.jsonc";
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read and validate `<dir>/pithy.worker.jsonc`, or `null` when the file is absent. A present-but-malformed
|
|
100
|
+
* manifest throws (never silently defaulted) — a typo in `readySignal` or `autostart` must surface, not be
|
|
101
|
+
* swallowed into a worker that quietly never starts.
|
|
102
|
+
*/
|
|
103
|
+
export async function parseWorkerManifest(dir: string): Promise<WorkerManifest | null> {
|
|
104
|
+
let raw: string;
|
|
105
|
+
try {
|
|
106
|
+
raw = await readFile(join(dir, WORKER_MANIFEST_FILE), "utf8");
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let value: unknown;
|
|
112
|
+
try {
|
|
113
|
+
value = parse(raw);
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
throw new InternalError({
|
|
116
|
+
message: `${WORKER_MANIFEST_FILE} in ${dir} is not valid JSONC.`,
|
|
117
|
+
action: "Fix the syntax, or delete it to accept the defaults.",
|
|
118
|
+
detail: cause instanceof Error ? cause.message : String(cause),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = WorkerManifest.safeParse(value);
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
throw fromZodError(result.error, {
|
|
125
|
+
message: `${WORKER_MANIFEST_FILE} in ${dir} is invalid.`,
|
|
126
|
+
action: "Correct the dev block, or delete the file to accept the defaults.",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return result.data;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The dev block for a worker with no manifest — its one/legacy worker, so it autostarts. */
|
|
133
|
+
export function defaultWorkerDev(): WorkerDev {
|
|
134
|
+
return WorkerDev.parse({ autostart: true });
|
|
135
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { ValidationError } from "@pithy-sh/core/src/error/pithyError";
|
|
7
|
+
import { DEFAULT_ENVIRONMENTS } from "@pithy-sh/core/src/naming/environment";
|
|
8
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from "@pithy-sh/core/src/version.generated";
|
|
9
|
+
import { ensureEmptyTarget, kitRange, WORKER_NAME, workerNamespace } from "./scaffold";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The wrangler every producer of a Worker manifest pins.
|
|
13
|
+
*
|
|
14
|
+
* Three write it: this file, `templates/starter/apps/api/package.json`, and the React stub `pithy ui add`
|
|
15
|
+
* merges into a Worker. `scaffoldParity.test.ts` held it between the first two only, so the third could
|
|
16
|
+
* drift from both unnoticed — a Worker whose front end and whose server deploy through different wrangler
|
|
17
|
+
* majors. Exported and imported rather than restated, so there is one literal and one test holding it to
|
|
18
|
+
* the template on disk.
|
|
19
|
+
*
|
|
20
|
+
* `@cloudflare/vite-plugin` peers on wrangler, so this range is not free to move alone. Checked at
|
|
21
|
+
* 4.123.0: plugin 1.48.0 peers `^4.115.0` and plugin 1.52.1 peers `^4.123.0`, and the stub's `^1.48.0`
|
|
22
|
+
* resolves the latter. Both are satisfied. A wrangler **major** still needs the plugin moved with it.
|
|
23
|
+
*/
|
|
24
|
+
export const WRANGLER_RANGE = "^4.123.0";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The files `scaffoldWorker` stamps into `apps/<name>/`, generated inline (no template dir to resolve).
|
|
28
|
+
*
|
|
29
|
+
* The deploy name is `<project>-<name>`, the same shape `scaffoldProject` gives the first Worker — a Worker
|
|
30
|
+
* script name is an account-flat Cloudflare name (CLAUDE.md §Resource naming), so the project segment is
|
|
31
|
+
* the only thing keeping two projects apart. Stamping the bare directory name meant two projects that each
|
|
32
|
+
* ran `pithy worker add admin` deployed to one script called `admin`, and the second `wrangler deploy`
|
|
33
|
+
* silently replaced the first's live Worker.
|
|
34
|
+
*/
|
|
35
|
+
function workerFiles(name: string, project: string, environments: readonly string[]): Record<string, string> {
|
|
36
|
+
// Built before the template rather than inside it: one stanza per declared environment, in declaration
|
|
37
|
+
// order. `env.<name>.vars` REPLACES the top-level block, so every stanza repeats all three vars.
|
|
38
|
+
const envStanzas = environments
|
|
39
|
+
.map((environment) =>
|
|
40
|
+
[
|
|
41
|
+
` "${environment}": {`,
|
|
42
|
+
` "vars": {`,
|
|
43
|
+
` "ENVIRONMENT": "${environment}",`,
|
|
44
|
+
` "PROJECT": "${project}",`,
|
|
45
|
+
` "WORKER": "${name}"`,
|
|
46
|
+
` }`,
|
|
47
|
+
` }`,
|
|
48
|
+
].join("\n"),
|
|
49
|
+
)
|
|
50
|
+
.join(",\n");
|
|
51
|
+
const wrangler = `{
|
|
52
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
53
|
+
"name": "${project}-${name}",
|
|
54
|
+
"main": "src/index.ts",
|
|
55
|
+
"compatibility_date": "2026-06-01",
|
|
56
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
57
|
+
|
|
58
|
+
// Workers Logs, on by default (see the root worker for the full note).
|
|
59
|
+
"observability": {
|
|
60
|
+
"enabled": true,
|
|
61
|
+
"head_sampling_rate": 1
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// The deployed version of this Worker, injected by Cloudflare. Top level, so every environment
|
|
65
|
+
// inherits it. It is what puts a build id on every log record and audit event, and what
|
|
66
|
+
// \`pithy deploy\` reads back to prove this Worker is the one answering at your domain.
|
|
67
|
+
"version_metadata": { "binding": "CF_VERSION_METADATA" },
|
|
68
|
+
|
|
69
|
+
// The top level is the dev environment; each \`env.<name>\` is one this project declares.
|
|
70
|
+
// All three vars repeat per environment: \`env.<name>.vars\` REPLACES this block rather than merging it.
|
|
71
|
+
// \`PROJECT\` is the root pithy.config.ts name — the owner stamped into every Cloudflare Images and
|
|
72
|
+
// Stream asset this Worker mints, since those stores are account-flat and key assets by their own id.
|
|
73
|
+
// \`WORKER\` is this Worker's own apps/<name> directory. The runtime tells a script nothing about
|
|
74
|
+
// itself, and Workers sharing a binding share one database, so this is what tells their events apart.
|
|
75
|
+
"vars": {
|
|
76
|
+
"ENVIRONMENT": "dev",
|
|
77
|
+
"PROJECT": "${project}",
|
|
78
|
+
"WORKER": "${name}"
|
|
79
|
+
},
|
|
80
|
+
"env": {
|
|
81
|
+
${envStanzas}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const manifest = `{
|
|
87
|
+
// How \`pithy dev\` runs this worker locally. This file is yours; wrangler.jsonc stays wrangler's.
|
|
88
|
+
"dev": {
|
|
89
|
+
// Must this run for the local environment to function? pithy dev starts exactly the autostart workers.
|
|
90
|
+
"autostart": true,
|
|
91
|
+
// Regex marking "ready" in this worker's output.
|
|
92
|
+
"readySignal": "Ready on https?://"
|
|
93
|
+
// "preferredPort": 8787 // a hint only — the feature's reserved port block is authoritative.
|
|
94
|
+
// "command": ["bun", "run", "dev"] // set this for a non-Worker process (e.g. a Vite frontend).
|
|
95
|
+
// {port} in any element of command becomes this worker's pinned port. pithy dev spawns with no
|
|
96
|
+
// shell, so an env var like $PORT on the argv would stay a literal.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
// The kit range, by the same rule `pithy init` stamps into the first Worker — `kitRange` imported from
|
|
102
|
+
// there rather than restated, because a second copy of the rule is a second thing to forget. `null`
|
|
103
|
+
// while nothing under `@pithy-sh/*` is published, and then the line is omitted outright: a `"^0.0.0"`
|
|
104
|
+
// here 404'd the install this command runs, which is how the second Worker in a project came out
|
|
105
|
+
// half-made. A published version writes the range with no change to this file.
|
|
106
|
+
const kit = kitRange(PACKAGE_VERSION);
|
|
107
|
+
const pkg = `${JSON.stringify(
|
|
108
|
+
{
|
|
109
|
+
// `<project>-<worker>`, matching both the deploy name above and what `pithy init` gives `apps/api`,
|
|
110
|
+
// so a workspace never holds two `admin` packages when a second project is checked out beside it.
|
|
111
|
+
name: `${project}-${name}`,
|
|
112
|
+
private: true,
|
|
113
|
+
type: "module",
|
|
114
|
+
// Every field below is held equal to the starter template's by `scaffoldParity.test.ts`. This file
|
|
115
|
+
// and that template are the project's two Worker producers, and only one of them is ever the file
|
|
116
|
+
// somebody remembers to edit — which is how the pins here fell a major version behind.
|
|
117
|
+
engines: { node: ">=22" },
|
|
118
|
+
scripts: {
|
|
119
|
+
dev: "wrangler dev",
|
|
120
|
+
deploy: "wrangler deploy",
|
|
121
|
+
"deploy:staging": "wrangler deploy --env staging",
|
|
122
|
+
"deploy:prod": "wrangler deploy --env prod",
|
|
123
|
+
},
|
|
124
|
+
dependencies: { ...(kit === null ? {} : { [PACKAGE_NAME]: kit }), hono: "^4.13.2" },
|
|
125
|
+
devDependencies: { "@cloudflare/workers-types": "^5.20260729.1", wrangler: WRANGLER_RANGE },
|
|
126
|
+
},
|
|
127
|
+
null,
|
|
128
|
+
2,
|
|
129
|
+
)}\n`;
|
|
130
|
+
|
|
131
|
+
// Same settings as the starter's `apps/api/tsconfig.json` — every Worker in a project typechecks alike,
|
|
132
|
+
// with the Workers globals its `src/index.ts` is written against. Workers have no root config to inherit.
|
|
133
|
+
const tsconfig = `{
|
|
134
|
+
// Matches the Pithy toolchain defaults: strict, ESM-only, TS-7 ready.
|
|
135
|
+
"compilerOptions": {
|
|
136
|
+
"target": "ES2022",
|
|
137
|
+
"lib": ["ES2022"],
|
|
138
|
+
"module": "ESNext",
|
|
139
|
+
"moduleResolution": "Bundler",
|
|
140
|
+
"moduleDetection": "force",
|
|
141
|
+
"verbatimModuleSyntax": true,
|
|
142
|
+
"isolatedModules": true,
|
|
143
|
+
"esModuleInterop": true,
|
|
144
|
+
"strict": true,
|
|
145
|
+
"noImplicitOverride": true,
|
|
146
|
+
"noUncheckedIndexedAccess": true,
|
|
147
|
+
"skipLibCheck": true,
|
|
148
|
+
"types": ["@cloudflare/workers-types"],
|
|
149
|
+
// Referenced from the root solution file, which requires \`composite\` — and \`composite\` makes tsc
|
|
150
|
+
// write build state. It goes under the PROJECT's \`dist/\`, already gitignored, and deliberately not
|
|
151
|
+
// under this Worker's: once this Worker carries a UI, Vite owns \`apps/<worker>/dist\` and empties it
|
|
152
|
+
// on every build, which would throw the incremental state away each time.
|
|
153
|
+
"composite": true,
|
|
154
|
+
"tsBuildInfoFile": "../../dist/${name}.server.tsbuildinfo",
|
|
155
|
+
"noEmit": true
|
|
156
|
+
},
|
|
157
|
+
"include": ["src/**/*.ts", "pithy.config.ts"]
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
const config = `// apps/${name}/pithy.config.ts — what THIS Worker is made of.
|
|
162
|
+
//
|
|
163
|
+
// Every Worker under apps/ has its own config. Capabilities are per-Worker because everything
|
|
164
|
+
// they drive is per-Worker: the composed route tree, the bindings written into this Worker's
|
|
165
|
+
// wrangler.jsonc, and Durable Object class migrations (which register a class against a script).
|
|
166
|
+
//
|
|
167
|
+
// Two Workers share a resource by declaring the SAME binding name — feature resource names are
|
|
168
|
+
// derived from the binding, not the Worker — so two Workers that both declare \`DB\` are backed by
|
|
169
|
+
// one D1. A Worker that wants its own declares a different binding (e.g. ${name.toUpperCase().replace(/-/g, "_")}_DB).
|
|
170
|
+
|
|
171
|
+
import { defineCapability } from "@pithy-sh/core/src/capability/capability";
|
|
172
|
+
import { compositionEnvironment } from "@pithy-sh/core/src/env/ambient";
|
|
173
|
+
import { originFor } from "@pithy-sh/core/src/naming/domains";
|
|
174
|
+
|
|
175
|
+
// Where this Worker answers, per environment. Declare it once here and the \`routes\` entry and
|
|
176
|
+
// \`vars.BASE_URL\` in wrangler.jsonc are generated from it — \`pithy worker add\` fills this in when you
|
|
177
|
+
// name a domain. \`dev\` is absent on purpose: local runs on the port your feature pinned.
|
|
178
|
+
const DOMAINS = {
|
|
179
|
+
// staging: { pattern: "staging.${name}.example.com", zone: "example.com" },
|
|
180
|
+
// prod: { pattern: "${name}.example.com", zone: "example.com" },
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// This Worker's public origin, for the environment it is composing in. Hand it to every capability that
|
|
184
|
+
// needs one — auth's callbacks, email's links, a payment's return URL — so no origin is ever written down.
|
|
185
|
+
//
|
|
186
|
+
// An origin written down is production's origin written into staging, and that is not a typo you notice:
|
|
187
|
+
// it is staging mailing your testers magic links into production. An environment DOMAINS does not name
|
|
188
|
+
// resolves to the local origin, so a link built in the wrong place goes nowhere rather than somewhere real.
|
|
189
|
+
//
|
|
190
|
+
// Exported so your own code can build a link against the same origin.
|
|
191
|
+
export const PUBLIC_ORIGIN = originFor(compositionEnvironment(), DOMAINS);
|
|
192
|
+
|
|
193
|
+
const app = defineCapability({
|
|
194
|
+
// The app's capability name. Also its migration namespace once it has tables, which is why it carries no
|
|
195
|
+
// hyphens: a namespace is lowercase letters and digits, starting with a letter.
|
|
196
|
+
name: "${workerNamespace(name)}",
|
|
197
|
+
// Bindings this Worker's own routes need beyond what capabilities declare.
|
|
198
|
+
requiredBindings: [],
|
|
199
|
+
// Mount your routes here. Every route declares how callers are verified.
|
|
200
|
+
// routes: (a) => {
|
|
201
|
+
// a.get("/hello", (c) => c.text("Hi."));
|
|
202
|
+
// },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const config = {
|
|
206
|
+
domains: DOMAINS,
|
|
207
|
+
// Library capabilities this Worker composes, in order.
|
|
208
|
+
// \`pithy add <capability> --worker ${name}\` registers them here.
|
|
209
|
+
capabilities: [
|
|
210
|
+
// pithy:capabilities (managed region — do not remove this marker)
|
|
211
|
+
],
|
|
212
|
+
app,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export default config;
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
const index = `import { createEntrypoint } from "@pithy-sh/core/src/createEntrypoint";
|
|
219
|
+
import config from "../pithy.config";
|
|
220
|
+
|
|
221
|
+
// The Worker. createEntrypoint assembles this Worker's capabilities into an entrypoint:
|
|
222
|
+
// \`fetch\` is one Hono app (typed db/kv registries per request, fail-fast binding validation,
|
|
223
|
+
// GET /health), and \`email\` fans inbound mail to every capability that handles it.
|
|
224
|
+
export default createEntrypoint(config);
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"wrangler.jsonc": wrangler,
|
|
229
|
+
"pithy.worker.jsonc": manifest,
|
|
230
|
+
"pithy.config.ts": config,
|
|
231
|
+
"package.json": pkg,
|
|
232
|
+
"tsconfig.json": tsconfig,
|
|
233
|
+
"src/index.ts": index,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Scaffold `apps/<name>/` — a new worker in the registry: its `wrangler.jsonc`, its `pithy.worker.jsonc`
|
|
239
|
+
* manifest, its `pithy.config.ts`, its `package.json`, its `tsconfig.json`, and a `src/index.ts` mount file.
|
|
240
|
+
* The same set `pithy init` gives `apps/api`, so a worker added later is not a second-class one on different
|
|
241
|
+
* TypeScript settings. Additive: it never touches any sibling. The `.dev.vars` symlink and the port
|
|
242
|
+
* reconcile are the command's job, not the scaffold's.
|
|
243
|
+
*
|
|
244
|
+
* `project` is the root `pithy.config.ts` name, resolved by the caller through `requireProjectName` (never
|
|
245
|
+
* guessed — CLAUDE.md §Resource naming). It leads the Worker's **deploy name** (`<project>-<name>`, an
|
|
246
|
+
* account-flat Cloudflare namespace) and its package name, and it is stamped as the `PROJECT` var in every
|
|
247
|
+
* environment stanza, which is what lets this Worker mint attributable Cloudflare Images and Stream assets.
|
|
248
|
+
*/
|
|
249
|
+
export async function scaffoldWorker(options: {
|
|
250
|
+
projectDir: string;
|
|
251
|
+
name: string;
|
|
252
|
+
project: string;
|
|
253
|
+
/**
|
|
254
|
+
* The project's declared environments (#241). Defaults to {@link DEFAULT_ENVIRONMENTS} for a caller
|
|
255
|
+
* with no config in hand; `addWorker` passes what the root `pithy.config.ts` says, so a second Worker
|
|
256
|
+
* declares the same environments as the first rather than the pair this file used to hardcode.
|
|
257
|
+
*/
|
|
258
|
+
environments?: readonly string[];
|
|
259
|
+
}): Promise<{ dir: string }> {
|
|
260
|
+
const environments = options.environments ?? DEFAULT_ENVIRONMENTS;
|
|
261
|
+
if (!WORKER_NAME.test(options.name)) {
|
|
262
|
+
throw new ValidationError({
|
|
263
|
+
message: `Worker name must be kebab-case (got "${options.name}").`,
|
|
264
|
+
action: "Use lowercase words joined by hyphens, e.g. web or admin-api.",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The gate runs **before** the directory is made, not after. A symlink anywhere between the project and
|
|
269
|
+
// `apps/<name>` is refused rather than created through (see `ensureScaffoldPath`), and a *dangling* one
|
|
270
|
+
// is ENOENT to a recursive `mkdir` — a raw node:fs error escaping the PithyError contract
|
|
271
|
+
// `withErrorReporting` prints from. Nothing created also means `addWorker` has nothing to roll back,
|
|
272
|
+
// which is the other half of #147's fix.
|
|
273
|
+
//
|
|
274
|
+
// The project directory is what bounds the walk, and passing it is the whole of #152 here: the gate used
|
|
275
|
+
// to be handed `apps/<name>` alone, so it lstat'd that one path and never looked at `apps`. A link there
|
|
276
|
+
// carried every file of the worker outside the project while the command printed the in-project path and
|
|
277
|
+
// exited 0 — `pithy init` already refused that shape, `worker add` did not.
|
|
278
|
+
const dir = join(options.projectDir, "apps", options.name);
|
|
279
|
+
await ensureEmptyTarget(options.projectDir, dir);
|
|
280
|
+
await mkdir(dir, { recursive: true });
|
|
281
|
+
|
|
282
|
+
const files = workerFiles(options.name, options.project, environments);
|
|
283
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
284
|
+
const path = join(dir, rel);
|
|
285
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
286
|
+
await writeFile(path, content);
|
|
287
|
+
}
|
|
288
|
+
return { dir };
|
|
289
|
+
}
|