@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,414 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { CLOUDFLARE_ENV_KEYS, parseDevVars } from "@pithy-sh/cloudflare/src/env/devVars";
|
|
7
|
+
import type { Capability } from "@pithy-sh/core/src/capability/capability";
|
|
8
|
+
import { cloudflareConfigPath } from "../cloudflare/config";
|
|
9
|
+
import { bootstrapVarsPath, readBootstrapVars } from "../devSecrets/bootstrapVars";
|
|
10
|
+
import { isGeneratedDevVars } from "../devSecrets/generate";
|
|
11
|
+
import { type DevSecretsTarget, resolveDevSecretsTargets, type UnresolvableWorker } from "../devSecrets/targets";
|
|
12
|
+
import type { StatePathOptions } from "../notifier/state";
|
|
13
|
+
import { loadProject, requireProjectName } from "../project/config";
|
|
14
|
+
import { resolveWorkers } from "../project/workerScope";
|
|
15
|
+
import { mintedTokensPath } from "../tokens/mintedTokens";
|
|
16
|
+
import { declaredVars } from "./wranglerVars";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The two `.dev.vars` questions nothing else in the toolchain asks: **is each Worker actually getting
|
|
20
|
+
* anything**, and **is the project root's file still holding values nothing reads** (#178).
|
|
21
|
+
*
|
|
22
|
+
* There are two `.dev.vars` in a Pithy project and they are not the same file:
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* <root>/.dev.vars hand-written. Read by the CLI, for CLOUDFLARE_ENV_KEYS and nothing else.
|
|
26
|
+
* <root>/apps/<w>/.dev.vars generated (#154). Read by wrangler, and so by the Worker.
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Since #154 the root file is **not a source** for the generated one — generation reads the dev secrets
|
|
30
|
+
* file and this machine's `dev.json`. So an upgraded project's secrets sit one file away from everything
|
|
31
|
+
* that would use them, every generated file comes out with a header and no values, and the first thing
|
|
32
|
+
* that says so is a 500 from a running Worker: `Missing required bindings: …`. Both facts are one `stat`
|
|
33
|
+
* and one parse away from a diagnostic, and `doctor` is the command whose whole job is to notice.
|
|
34
|
+
*
|
|
35
|
+
* **Every key in the root file is classified, and the classification is total.** That is what stops a
|
|
36
|
+
* new shape of key from arriving unreported, which is the failure this module exists for: the misplaced
|
|
37
|
+
* check next door reached `backend: "d1"` and every other kind of stranded value fell through it in
|
|
38
|
+
* silence. A key is either read from that file, reported by a neighboring check, or reported here.
|
|
39
|
+
*
|
|
40
|
+
* **Reported, never fixed, and never fatal to the exit.** #154 said migration is reported rather than
|
|
41
|
+
* automatic, and silently rewriting an adopter's `.dev.vars` is what #142 was about. Every project that
|
|
42
|
+
* predates the generated file is in this state by definition, so failing the exit would turn a green CI
|
|
43
|
+
* red over a file that used to work. Reported means reported, though — and until now nothing did.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Why a key is in the project root's `.dev.vars`, and whether anything reads it there.
|
|
48
|
+
*
|
|
49
|
+
* Declared as a tuple rather than a bare union so a test can iterate it: a state added without a
|
|
50
|
+
* sentence to print for it is a key class that would go silent, which is this module's whole subject.
|
|
51
|
+
*/
|
|
52
|
+
export const ROOT_DEV_VAR_STATES = [
|
|
53
|
+
/**
|
|
54
|
+
* One of {@link CLOUDFLARE_ENV_KEYS}. The CLI read these out of exactly this file until #182; they are
|
|
55
|
+
* account-scoped, so they live in `<config>/cloudflare.json` now and nothing reads this copy.
|
|
56
|
+
*/
|
|
57
|
+
"credential",
|
|
58
|
+
/**
|
|
59
|
+
* A secret some Worker's registry declares. It belongs in the dev secrets file whatever its backend,
|
|
60
|
+
* and `checkDevSecrets` is what says so — named here only so the classification stays total.
|
|
61
|
+
*/
|
|
62
|
+
"secret",
|
|
63
|
+
/**
|
|
64
|
+
* A name some Worker requires as a binding, or declares in its `wrangler.jsonc` `vars` — but not
|
|
65
|
+
* from this file, which no Worker reads. The value is real and its home is elsewhere.
|
|
66
|
+
*/
|
|
67
|
+
"binding",
|
|
68
|
+
/**
|
|
69
|
+
* A Worker's `pithy.config.ts` would not import, so **nothing here can say what reads this** (#208).
|
|
70
|
+
*
|
|
71
|
+
* The state that exists because `unread` is a *negative* claim — "nothing this project composes
|
|
72
|
+
* declares it" — and a registry nobody could read is exactly what might have declared it. Registries
|
|
73
|
+
* merge project-wide, so a healthy sibling does not settle it either: its registry is real, and it is
|
|
74
|
+
* not the project's answer. Until #208 an unresolvable config answered `[]` like a project with no
|
|
75
|
+
* secrets, so every key in the file classified `unread` and the report said "delete it" about the
|
|
76
|
+
* broken Worker's own secrets.
|
|
77
|
+
*
|
|
78
|
+
* The three states above survive a partial read because each is positive evidence: a fixed credential
|
|
79
|
+
* list, a registry that *did* declare the name, a composition that *does* want it. This one is what is
|
|
80
|
+
* left when the only remaining answer would have been an inference from files nobody could open.
|
|
81
|
+
*/
|
|
82
|
+
"unclassified",
|
|
83
|
+
/** Nothing reads it: not a credential, not declared by anything this project composes. */
|
|
84
|
+
"unread",
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
/** Why one root `.dev.vars` key is there. See {@link ROOT_DEV_VAR_STATES}. */
|
|
88
|
+
export type RootDevVarState = (typeof ROOT_DEV_VAR_STATES)[number];
|
|
89
|
+
|
|
90
|
+
/** One key in the project root's `.dev.vars`, and what reads it. Never its value. */
|
|
91
|
+
export interface RootDevVar {
|
|
92
|
+
/** The variable name, as the adopter wrote it. */
|
|
93
|
+
key: string;
|
|
94
|
+
/** What reads it, if anything. */
|
|
95
|
+
state: RootDevVarState;
|
|
96
|
+
/** The Workers that want it, for a `binding` key — so the sentence can name one. Sorted. */
|
|
97
|
+
workers: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** One Worker whose generated `.dev.vars` is a header and nothing else. */
|
|
101
|
+
export interface EmptyDevVars {
|
|
102
|
+
/** The Worker's name — the whole point of the finding is which one is getting nothing. */
|
|
103
|
+
worker: string;
|
|
104
|
+
/** The generated file, relative to the project root. */
|
|
105
|
+
file: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What doctor learned about this project's `.dev.vars` files. */
|
|
109
|
+
export interface DevVarsCheck {
|
|
110
|
+
/**
|
|
111
|
+
* Every key in the project root's `.dev.vars`, classified, sorted by key. **Total**: one entry per
|
|
112
|
+
* key in the file, so no key can be silently skipped. `credential` and `secret` print nothing.
|
|
113
|
+
*/
|
|
114
|
+
root: RootDevVar[];
|
|
115
|
+
/**
|
|
116
|
+
* Workers whose generated `.dev.vars` pithy wrote with no values in it. A Worker in this list cannot
|
|
117
|
+
* serve a request — it has none of its bindings — and nothing else in the toolchain would say so
|
|
118
|
+
* before the first 500.
|
|
119
|
+
*/
|
|
120
|
+
empty: EmptyDevVars[];
|
|
121
|
+
/**
|
|
122
|
+
* Where this machine's per-project dev values live — the file a `binding` key belongs in. `null`
|
|
123
|
+
* when the project has no name to key one on, which is the same condition every other per-project
|
|
124
|
+
* path in the report declines under.
|
|
125
|
+
*/
|
|
126
|
+
devConfigPath: string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Where a minted credential belongs — `<config>/<project>/tokens.json`. `null` when the project has
|
|
129
|
+
* no name to key one on, which is the same condition {@link devConfigPath} declines under.
|
|
130
|
+
*
|
|
131
|
+
* Carried because the sentence has to name it. Nothing copies an existing token into that file —
|
|
132
|
+
* `writeMintedToken` is reached only on a *fresh* mint — so the remedy is a person moving a line, and
|
|
133
|
+
* a remedy that cannot say where is not one.
|
|
134
|
+
*/
|
|
135
|
+
mintedTokensPath: string | null;
|
|
136
|
+
/**
|
|
137
|
+
* Minted-credential files left in the project by the old `dev-vars` token sink — `.dev.vars.<env>`,
|
|
138
|
+
* one per environment, each holding that environment's live Cloudflare token (#182). Sorted.
|
|
139
|
+
*
|
|
140
|
+
* **Named, never deleted.** #142 is why: a run that rewrote an adopter's `.dev.vars` deleted gitignored
|
|
141
|
+
* secrets with no copy anywhere. The value in a `.dev.vars.production` may be the only copy of a
|
|
142
|
+
* production credential that exists, and this report is the thing that tells somebody it is there.
|
|
143
|
+
*/
|
|
144
|
+
minted: MintedDevVarsFile[];
|
|
145
|
+
/**
|
|
146
|
+
* Registry secrets still sitting in `<config>/<project>/dev.json` under `vars` — the copy `pithy seed`
|
|
147
|
+
* used to make, which nothing reads since #179. Names only, sorted. Never a value.
|
|
148
|
+
*/
|
|
149
|
+
devJsonSecrets: string[];
|
|
150
|
+
/**
|
|
151
|
+
* Every Worker with a `pithy.config.ts` that would not import, and why (#208). Empty on an ordinary run.
|
|
152
|
+
*
|
|
153
|
+
* It is a field rather than an absence because an absence is what the defect was: the lossy target list
|
|
154
|
+
* answered `[]` for this state and for a project that never composed `secrets`, so every consumer read
|
|
155
|
+
* a config that would not load as a Worker that declares nothing. A caller now has to drop it on
|
|
156
|
+
* purpose. It also decides {@link RootDevVarState} — see `unclassified`.
|
|
157
|
+
*/
|
|
158
|
+
unresolvable: UnresolvableWorker[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** One `.dev.vars.<env>` the old token sink left in the checkout. */
|
|
162
|
+
export interface MintedDevVarsFile {
|
|
163
|
+
/** The file, relative to the project root. */
|
|
164
|
+
file: string;
|
|
165
|
+
/** The environment it was minted for — the whole point of naming it. */
|
|
166
|
+
env: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** What {@link checkDevVars} needs. Every seam defaults to the real project. */
|
|
170
|
+
export interface CheckDevVarsOptions {
|
|
171
|
+
/** The project root — owner of the hand-written `.dev.vars` and of `apps/`. */
|
|
172
|
+
projectDir: string;
|
|
173
|
+
/** The Workers whose registries declare the secrets. Defaults to every one composing `secrets`. */
|
|
174
|
+
targets?: DevSecretsTarget[];
|
|
175
|
+
/**
|
|
176
|
+
* The Workers whose `pithy.config.ts` would not import. Read only when {@link targets} is supplied —
|
|
177
|
+
* both halves of one resolution, so a seam cannot state one and let the other default to a lie.
|
|
178
|
+
*/
|
|
179
|
+
unresolvable?: UnresolvableWorker[];
|
|
180
|
+
/** The Workers whose bindings are read and whose generated files are checked. Defaults to all of them. */
|
|
181
|
+
workers?: { name: string; dir: string; capabilities: Capability[] }[];
|
|
182
|
+
/** Where the Pithy config directory is. Defaults to the real one; a seam so a test reads its own. */
|
|
183
|
+
paths?: StatePathOptions;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read both scopes and answer both questions. Never throws — a diagnostic has to work in the broken
|
|
188
|
+
* environment it exists to diagnose, and every one of these reads is of a file that may not be there.
|
|
189
|
+
*/
|
|
190
|
+
export async function checkDevVars(options: CheckDevVarsOptions): Promise<DevVarsCheck> {
|
|
191
|
+
const workers =
|
|
192
|
+
options.workers ??
|
|
193
|
+
(await resolveWorkers({ projectDir: options.projectDir }).catch(() => [])).map((worker) => ({
|
|
194
|
+
name: worker.name,
|
|
195
|
+
dir: worker.dir,
|
|
196
|
+
capabilities: worker.capabilities,
|
|
197
|
+
}));
|
|
198
|
+
// Both halves of one resolution (#208). `resolveDevSecretsTargets` cannot drop the failure, because
|
|
199
|
+
// the failure is a field — the lossy wrapper's `.catch(() => [])` used to stand here and answered a
|
|
200
|
+
// config that would not import exactly as it answered a project with no secrets.
|
|
201
|
+
const { targets, unresolvable } =
|
|
202
|
+
options.targets === undefined
|
|
203
|
+
? await resolveDevSecretsTargets(options.projectDir)
|
|
204
|
+
: { targets: options.targets, unresolvable: options.unresolvable ?? [] };
|
|
205
|
+
|
|
206
|
+
// What the registries declare, and what the compositions require. Two different answers to "does
|
|
207
|
+
// anything read this name", kept apart because they have two different fixes.
|
|
208
|
+
const declaredSecrets = new Set(targets.flatMap((target) => Object.keys(target.registry)));
|
|
209
|
+
const wants = new Map<string, string[]>();
|
|
210
|
+
for (const worker of workers) {
|
|
211
|
+
const names = new Set<string>();
|
|
212
|
+
for (const capability of worker.capabilities) {
|
|
213
|
+
for (const binding of capability.requiredBindings ?? []) names.add(binding.name);
|
|
214
|
+
}
|
|
215
|
+
for (const name of await declaredVars(worker.dir)) names.add(name);
|
|
216
|
+
for (const name of names) wants.set(name, [...(wants.get(name) ?? []), worker.name].sort());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const inRoot = parseDevVars(await readFile(join(options.projectDir, ".dev.vars"), "utf8").catch(() => ""));
|
|
220
|
+
const root: RootDevVar[] = Object.keys(inRoot)
|
|
221
|
+
.sort()
|
|
222
|
+
.map((key) => ({
|
|
223
|
+
key,
|
|
224
|
+
state: classify(key, declaredSecrets, wants, unresolvable.length > 0),
|
|
225
|
+
workers: wants.get(key) ?? [],
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
const empty: EmptyDevVars[] = [];
|
|
229
|
+
for (const worker of workers) {
|
|
230
|
+
const path = join(worker.dir, ".dev.vars");
|
|
231
|
+
const source = await readFile(path, "utf8").catch(() => null);
|
|
232
|
+
// No file is not a finding: nothing has generated one yet, and the first `pithy dev` will. A file
|
|
233
|
+
// pithy did not write is the adopter's own — `generateDevVars` refuses to touch it, and judging its
|
|
234
|
+
// contents here would be the same overreach one report further along.
|
|
235
|
+
if (source === null || !isGeneratedDevVars(source)) continue;
|
|
236
|
+
if (Object.keys(parseDevVars(source)).length > 0) continue;
|
|
237
|
+
empty.push({ worker: worker.name, file: relative(options.projectDir, path) });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// The registry is what says a `dev.json` value has a better home now. A name nothing declares is a
|
|
241
|
+
// legitimate machine-local variable — a Turnstile sitekey — and `dev.json` is still where it belongs.
|
|
242
|
+
const recorded = await readBootstrapVars(options.projectDir, options.paths ?? {}).catch(() => ({}));
|
|
243
|
+
const devJsonSecrets = Object.keys(recorded).filter((key) => declaredSecrets.has(key));
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
root,
|
|
247
|
+
empty: empty.sort((a, b) => a.worker.localeCompare(b.worker)),
|
|
248
|
+
devConfigPath: await bootstrapVarsPath(options.projectDir, options.paths ?? {}).catch(() => null),
|
|
249
|
+
mintedTokensPath: await tokensPath(options.projectDir, options.paths ?? {}),
|
|
250
|
+
minted: await mintedDevVarsFiles(options.projectDir),
|
|
251
|
+
devJsonSecrets: devJsonSecrets.sort(),
|
|
252
|
+
unresolvable,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* `<config>/<project>/tokens.json` for a project root, or `null` when the project has no name.
|
|
258
|
+
*
|
|
259
|
+
* Composed through `mintedTokensPath` rather than assembled beside `devConfigPath`'s directory: the two
|
|
260
|
+
* files are siblings today, and a path built from that fact would be a second producer of a name this
|
|
261
|
+
* repo keeps in one place.
|
|
262
|
+
*/
|
|
263
|
+
async function tokensPath(projectDir: string, options: StatePathOptions): Promise<string | null> {
|
|
264
|
+
try {
|
|
265
|
+
return mintedTokensPath(requireProjectName(await loadProject(projectDir)), options);
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Every `.dev.vars.<env>` still in the project root.
|
|
273
|
+
*
|
|
274
|
+
* Read by listing the directory rather than by composing a name per known environment: the old sink
|
|
275
|
+
* wrote whatever `--env` it was handed, so a project can hold a `.dev.vars.qa` no list here would guess.
|
|
276
|
+
* `.dev.vars` and `.dev.vars.local` and `.dev.vars.example` are all other things and are not this.
|
|
277
|
+
*/
|
|
278
|
+
async function mintedDevVarsFiles(projectDir: string): Promise<MintedDevVarsFile[]> {
|
|
279
|
+
const entries = await readdir(projectDir, { withFileTypes: true }).catch(() => []);
|
|
280
|
+
const found: MintedDevVarsFile[] = [];
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
if (!entry.isFile()) continue;
|
|
283
|
+
const env = /^\.dev\.vars\.(.+)$/.exec(entry.name)?.[1];
|
|
284
|
+
if (env === undefined || env === "local" || env === "example") continue;
|
|
285
|
+
found.push({ file: entry.name, env });
|
|
286
|
+
}
|
|
287
|
+
return found.sort((a, b) => a.file.localeCompare(b.file));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Whether the CLI itself reads this name out of the project root's `.dev.vars`.
|
|
292
|
+
*
|
|
293
|
+
* **The one place that answers it, because the answer used to be inferred from `backend`.** A
|
|
294
|
+
* `cf-secrets-store` secret was left unreported there on the grounds that `CLOUDFLARE_API_TOKEN` has
|
|
295
|
+
* no local store to live in — true of that one name, and not true of the backend. The dashboard's own
|
|
296
|
+
* `cf-secrets-store` secrets sat stranded in that file with nothing to say so (#178). The list the
|
|
297
|
+
* readers use is the list that decides, and it is `CLOUDFLARE_ENV_KEYS`.
|
|
298
|
+
*/
|
|
299
|
+
export function isCloudflareEnvKey(name: string): boolean {
|
|
300
|
+
return (CLOUDFLARE_ENV_KEYS as readonly string[]).includes(name);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* One root key's state. Ordered by who reads it: the CLI, then a registry, then a composition.
|
|
305
|
+
*
|
|
306
|
+
* The first three are positive evidence and survive a `partial` read — a fixed credential list, a
|
|
307
|
+
* registry that did declare the name, a composition that does want it. `unread` is the one negative
|
|
308
|
+
* claim, so with a Worker nobody could ask it becomes `unclassified` instead (#208).
|
|
309
|
+
*/
|
|
310
|
+
function classify(
|
|
311
|
+
key: string,
|
|
312
|
+
declaredSecrets: Set<string>,
|
|
313
|
+
wants: Map<string, string[]>,
|
|
314
|
+
partial: boolean,
|
|
315
|
+
): RootDevVarState {
|
|
316
|
+
if (isCloudflareEnvKey(key)) return "credential";
|
|
317
|
+
if (declaredSecrets.has(key)) return "secret";
|
|
318
|
+
if (wants.has(key)) return "binding";
|
|
319
|
+
return partial ? "unclassified" : "unread";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The lines the report prints, or none at all when there is nothing to say.
|
|
324
|
+
*
|
|
325
|
+
* The empty-Worker lines come first because they are the loudest thing `doctor` can say about a dev
|
|
326
|
+
* environment: that Worker answers every request with a missing-binding error, and the root-file lines
|
|
327
|
+
* below are usually the reason why.
|
|
328
|
+
*/
|
|
329
|
+
export function describeDevVars(check: DevVarsCheck): string[] {
|
|
330
|
+
const lines: string[] = [];
|
|
331
|
+
// First, above everything, because it is the sentence that explains the lines under it: a Worker
|
|
332
|
+
// nobody could ask is why half of them say "unclassified" rather than naming a destination. `doctor`
|
|
333
|
+
// is run *because* something is already wrong, so this is a finding and never a refusal — the rest of
|
|
334
|
+
// the block still prints (#208).
|
|
335
|
+
for (const worker of check.unresolvable) {
|
|
336
|
+
lines.push(
|
|
337
|
+
`${worker.name}'s pithy.config.ts would not import, so nothing here knows what it declares. ${worker.reason}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
for (const { file, env } of check.minted) {
|
|
341
|
+
const home = check.mintedTokensPath === null ? "this project's config directory" : check.mintedTokensPath;
|
|
342
|
+
lines.push(
|
|
343
|
+
`${file} holds a credential minted for ${env}, inside the checkout. Nothing writes one there now — move each line into ${home} as { "${env}": { "<NAME>": "<value>" } }, chmod 600 it and 700 its directory, then delete the file. Gitignored is not enough: npm pack does not read .gitignore.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (check.devJsonSecrets.length > 0) {
|
|
347
|
+
const home = check.devConfigPath === null ? "this machine's dev config" : check.devConfigPath;
|
|
348
|
+
lines.push(
|
|
349
|
+
`${check.devJsonSecrets.join(", ")} ${check.devJsonSecrets.length === 1 ? "is" : "are"} in ${home} under "vars". Nothing reads that copy — the value belongs in the dev secrets file. Run pithy secrets edit to put it there, then delete the copy.`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
for (const { worker, file } of check.empty) {
|
|
353
|
+
lines.push(
|
|
354
|
+
`${worker} has no dev values: ${file} was generated with none. That Worker starts with none of its bindings. Run pithy seed.`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
for (const entry of check.root) {
|
|
358
|
+
const line = describeRootDevVar(entry, check.devConfigPath, check.unresolvable);
|
|
359
|
+
if (line !== null) lines.push(line);
|
|
360
|
+
}
|
|
361
|
+
return lines;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* One root key's sentence, or `null` when the key is where it belongs or is another check's to report.
|
|
366
|
+
*
|
|
367
|
+
* A switch over {@link RootDevVarState} with no default, so adding a state is a type error here rather
|
|
368
|
+
* than a key class that goes quiet — which is exactly how the `d1`-only check lost the other backend.
|
|
369
|
+
*/
|
|
370
|
+
function describeRootDevVar(
|
|
371
|
+
entry: RootDevVar,
|
|
372
|
+
devConfigPath: string | null,
|
|
373
|
+
unresolvable: UnresolvableWorker[],
|
|
374
|
+
): string | null {
|
|
375
|
+
switch (entry.state) {
|
|
376
|
+
case "credential":
|
|
377
|
+
// Named, not deleted for them. The value is real and it is a live Cloudflare credential — the one
|
|
378
|
+
// class of value in this file that is worth naming twice rather than removing on somebody's behalf.
|
|
379
|
+
return `${entry.key} is in .dev.vars, which nothing reads now. It is account-scoped — copy it into ${cloudflareConfigPath({ account: null })} as { "${entry.key}": "<value>" }, chmod 600, or export it.`;
|
|
380
|
+
case "secret":
|
|
381
|
+
return null; // `describeDevSecrets` names it, and says which file it belongs in.
|
|
382
|
+
case "binding": {
|
|
383
|
+
// The shape and the mode are stated because nothing else will write that file. No `pithy` subcommand
|
|
384
|
+
// takes a name and a value for `dev.json`'s `vars` — `writeBootstrapVars` is reached only from
|
|
385
|
+
// provisioning and mint paths — so the remedy is a person editing JSON. Every writer of these files
|
|
386
|
+
// creates them `0600` in a `0700` directory (#182); a hand-written one lands at the umask default
|
|
387
|
+
// and nothing here checks it afterwards, so the mode belongs in the sentence.
|
|
388
|
+
const wanted = entry.workers.length > 0 ? entry.workers.join(", ") : "this project";
|
|
389
|
+
const home = devConfigPath === null ? "this machine's dev config" : devConfigPath;
|
|
390
|
+
return `${entry.key} is in .dev.vars, which no Worker reads. ${wanted} needs it as a binding — its dev value belongs in ${home}, as { "vars": { "${entry.key}": "<value>" } }, chmod 600.`;
|
|
391
|
+
}
|
|
392
|
+
case "unclassified": {
|
|
393
|
+
// Named, never advised on. The one thing this line must not do is repeat what `unread` says: the
|
|
394
|
+
// whole point is that "nothing reads it, delete it" is the claim nobody could establish, and this
|
|
395
|
+
// key may be the unreadable Worker's own registry secret.
|
|
396
|
+
const named = unresolvable.map((worker) => worker.name).join(", ");
|
|
397
|
+
const who = named === "" ? "a Worker whose pithy.config.ts will not import" : named;
|
|
398
|
+
return `${entry.key} is in .dev.vars, and nothing here can say what reads it while ${who} will not import. Fix that first.`;
|
|
399
|
+
}
|
|
400
|
+
case "unread":
|
|
401
|
+
return `${entry.key} is in .dev.vars and nothing reads it — not a Cloudflare credential, and nothing this project composes declares it. Delete it.`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Whether there is anything here to *fix*.
|
|
407
|
+
*
|
|
408
|
+
* Every finding this module produces is one, which is why there is no `devSecretsHealthy`-shaped
|
|
409
|
+
* exception list: a Worker with no bindings and a value nothing reads are both faults. Neither fails
|
|
410
|
+
* the exit — see the module note — but both are worth dragging the report verbose for.
|
|
411
|
+
*/
|
|
412
|
+
export function devVarsHealthy(check: DevVarsCheck): boolean {
|
|
413
|
+
return describeDevVars(check).length === 0;
|
|
414
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { relative } from "node:path";
|
|
5
|
+
import { DEV_VARS_LOCAL, readLocalOverrides } from "../devSecrets/generate";
|
|
6
|
+
import { type DevSecretsTarget, resolveDevSecretsTargets, type UnresolvableWorker } from "../devSecrets/targets";
|
|
7
|
+
import { discoverWorkers } from "../project/workers";
|
|
8
|
+
import { declaredVars } from "./wranglerVars";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What is in a `.dev.vars.local` that nothing else in the project knows about — the footgun `.local`
|
|
12
|
+
* creates, made visible (#154).
|
|
13
|
+
*
|
|
14
|
+
* `.dev.vars.local` is for **overrides**, not for variables. Overriding a generated value locally, or
|
|
15
|
+
* shadowing a secret for an afternoon, is exactly what it is for. But a variable that exists *only*
|
|
16
|
+
* there works in dev and is simply absent in production, and that failure lands at deploy — far from the
|
|
17
|
+
* cause, in a file the deploy never reads and git never showed anyone. Anything an adopter wants to
|
|
18
|
+
* exist belongs in `wrangler.jsonc`'s `vars`: committed, reviewed, and deployed with the Worker.
|
|
19
|
+
*
|
|
20
|
+
* **Visible, not forbidden.** Neither finding fails the exit. A shadowing override is legitimate and
|
|
21
|
+
* common; a dev-only variable is sometimes genuinely dev-only. What is not acceptable is that either be
|
|
22
|
+
* invisible, which is what a git-ignored file is by construction.
|
|
23
|
+
*
|
|
24
|
+
* **What counts as declared is {@link declaredVars}'s to say**, and it is shared with the root
|
|
25
|
+
* `.dev.vars` check next door — `env.<name>.vars` *replaces* the top-level block rather than merging it,
|
|
26
|
+
* so a key counts if it appears at the top level **or** in any environment. Reading only the top level
|
|
27
|
+
* names a staging-only variable as dev-only, and reading only one environment names every ordinary
|
|
28
|
+
* variable. Two checks asking the same question, through one function, so only one can be half-right.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** One `.dev.vars.local` key with nothing behind it, and where it was written. */
|
|
32
|
+
export interface DevOnlyVar {
|
|
33
|
+
/** The variable name, as the adopter wrote it. Never its value. */
|
|
34
|
+
key: string;
|
|
35
|
+
/** The `.dev.vars.local` it is in, relative to the project root — root's, or a Worker's. */
|
|
36
|
+
file: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What doctor learned about this project's `.dev.vars.local` files. */
|
|
40
|
+
export interface DevVarsLocalCheck {
|
|
41
|
+
/**
|
|
42
|
+
* Keys that are neither a registry secret nor declared in any `wrangler.jsonc` `vars` block. They
|
|
43
|
+
* exist in dev and nowhere else. Sorted by file, then key.
|
|
44
|
+
*
|
|
45
|
+
* **Empty whenever anything is {@link DevVarsLocalCheck.unresolvable}** (#208). "Nowhere else knows
|
|
46
|
+
* about this key" is a negative claim, and a registry that would not load is exactly what might have
|
|
47
|
+
* known. Saying it anyway is how an adopter is told a declared secret is dev-only.
|
|
48
|
+
*/
|
|
49
|
+
devOnly: DevOnlyVar[];
|
|
50
|
+
/**
|
|
51
|
+
* Keys that shadow a registry secret. Legitimate — that is what the file is for — and never invisible:
|
|
52
|
+
* a Worker running on a value that is not the one the secrets file states is a fact worth one line.
|
|
53
|
+
*/
|
|
54
|
+
shadowing: DevOnlyVar[];
|
|
55
|
+
/**
|
|
56
|
+
* Every Worker with a `pithy.config.ts` that would not import, and why (#208). Empty on an ordinary run.
|
|
57
|
+
*
|
|
58
|
+
* The sentence naming it belongs to `./devVars`, which prints first in the same `Dev secrets:` block;
|
|
59
|
+
* this field is what stops this check from making a claim about a registry it never read.
|
|
60
|
+
*/
|
|
61
|
+
unresolvable: UnresolvableWorker[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What {@link checkDevVarsLocal} needs. Every seam defaults to the real project. */
|
|
65
|
+
export interface CheckDevVarsLocalOptions {
|
|
66
|
+
/** The project root — owner of the root `.dev.vars.local` and of `apps/`. */
|
|
67
|
+
projectDir: string;
|
|
68
|
+
/** The Workers whose registries declare the secrets. Defaults to every one composing `secrets`. */
|
|
69
|
+
targets?: DevSecretsTarget[];
|
|
70
|
+
/**
|
|
71
|
+
* The Workers whose `pithy.config.ts` would not import. Read only when {@link targets} is supplied —
|
|
72
|
+
* both halves of one resolution, so a seam cannot state one and let the other default to a lie.
|
|
73
|
+
*/
|
|
74
|
+
unresolvable?: UnresolvableWorker[];
|
|
75
|
+
/** The Worker directories to look in. Defaults to every discovered Worker. */
|
|
76
|
+
workerDirs?: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read both scopes and judge each key against the registry and against `wrangler.jsonc`. Never throws — a
|
|
81
|
+
* diagnostic has to work in the broken environment it exists to diagnose. `null` when there is nothing to
|
|
82
|
+
* say, which is every project with no `.dev.vars.local` anywhere: the overwhelming majority.
|
|
83
|
+
*/
|
|
84
|
+
export async function checkDevVarsLocal(options: CheckDevVarsLocalOptions): Promise<DevVarsLocalCheck | null> {
|
|
85
|
+
const workerDirs =
|
|
86
|
+
options.workerDirs ?? (await discoverWorkers(options.projectDir).catch(() => [])).map((w) => w.dir);
|
|
87
|
+
// Both halves of one resolution (#208) — the lossy wrapper's `.catch(() => [])` stood here and read a
|
|
88
|
+
// config that would not import as a Worker that declares nothing.
|
|
89
|
+
const { targets, unresolvable } =
|
|
90
|
+
options.targets === undefined
|
|
91
|
+
? await resolveDevSecretsTargets(options.projectDir)
|
|
92
|
+
: { targets: options.targets, unresolvable: options.unresolvable ?? [] };
|
|
93
|
+
const declaredSecrets = new Set(targets.flatMap((target) => Object.keys(target.registry)));
|
|
94
|
+
|
|
95
|
+
const devOnly: DevOnlyVar[] = [];
|
|
96
|
+
const shadowing: DevOnlyVar[] = [];
|
|
97
|
+
// The root file applies to every Worker, so its keys are judged against the union of every Worker's
|
|
98
|
+
// `vars`: a key declared by the one Worker that needs it is not a dev-only variable.
|
|
99
|
+
const everyVars = new Set<string>();
|
|
100
|
+
const perWorker = new Map<string, Set<string>>();
|
|
101
|
+
for (const dir of workerDirs) {
|
|
102
|
+
const vars = await declaredVars(dir);
|
|
103
|
+
perWorker.set(dir, vars);
|
|
104
|
+
for (const key of vars) everyVars.add(key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const scopes: { dir: string; declared: Set<string> }[] = [
|
|
108
|
+
{ dir: options.projectDir, declared: everyVars },
|
|
109
|
+
...workerDirs.map((dir) => ({ dir, declared: perWorker.get(dir) ?? new Set<string>() })),
|
|
110
|
+
];
|
|
111
|
+
for (const scope of scopes) {
|
|
112
|
+
const at = relative(options.projectDir, scope.dir);
|
|
113
|
+
const file = at === "" ? DEV_VARS_LOCAL : `${at}/${DEV_VARS_LOCAL}`;
|
|
114
|
+
for (const key of Object.keys(await readLocalOverrides(scope.dir).catch(() => ({}))).sort()) {
|
|
115
|
+
if (declaredSecrets.has(key)) shadowing.push({ key, file });
|
|
116
|
+
// The negative claim, and the one a partial resolution cannot support. Withheld rather than
|
|
117
|
+
// hedged: `doctor` already says "this Worker's config would not import" once, in this same block,
|
|
118
|
+
// and saying it again in other words per key is the noise `unreadable` next door avoids too.
|
|
119
|
+
else if (!scope.declared.has(key) && unresolvable.length === 0) devOnly.push({ key, file });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (devOnly.length === 0 && shadowing.length === 0 && unresolvable.length === 0) return null;
|
|
123
|
+
return { devOnly, shadowing, unresolvable };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The lines the report prints. Both findings are worth ink and neither is worth failing an exit over. */
|
|
127
|
+
export function describeDevVarsLocal(check: DevVarsLocalCheck): string[] {
|
|
128
|
+
const lines: string[] = [];
|
|
129
|
+
for (const { key, file } of check.devOnly) {
|
|
130
|
+
lines.push(
|
|
131
|
+
`${key} is in ${file} and nowhere else — it exists only in dev. A value production needs belongs in wrangler.jsonc vars.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
for (const { key, file } of check.shadowing) {
|
|
135
|
+
lines.push(`${key} in ${file} shadows the secret of that name. Fine, and never silent.`);
|
|
136
|
+
}
|
|
137
|
+
return lines;
|
|
138
|
+
}
|