@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,220 @@
|
|
|
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 { CloudflareClients } from "@pithy-sh/cloudflare/src/client/clients";
|
|
7
|
+
import type { ZoneInfo } from "@pithy-sh/cloudflare/src/zones/zonesManager";
|
|
8
|
+
import { ValidationError } from "@pithy-sh/core/src/error/pithyError";
|
|
9
|
+
import type { WorkerDomains } from "@pithy-sh/core/src/naming/domains";
|
|
10
|
+
import { type CloudflareAccountSelection, cloudflareEnv } from "../cloudflare/config";
|
|
11
|
+
import { applyDomains } from "./applyDomains";
|
|
12
|
+
import { writeFileAtomic } from "./atomic";
|
|
13
|
+
import {
|
|
14
|
+
buildDomains,
|
|
15
|
+
type DomainAnswer,
|
|
16
|
+
describeZone,
|
|
17
|
+
domainQuestions,
|
|
18
|
+
renderDomainsBlock,
|
|
19
|
+
renderDomainsConst,
|
|
20
|
+
zoneForHostname,
|
|
21
|
+
} from "./domainPrompt";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The interactive half of declaring where a Worker answers — the `@clack/prompts` calls `init` and
|
|
25
|
+
* `worker add` both make.
|
|
26
|
+
*
|
|
27
|
+
* The decisions live in `domainPrompt.ts`, which is pure and tested. This file is the I/O around them:
|
|
28
|
+
* fetching the account's zones when it can, asking, and reporting why it could not offer a picker.
|
|
29
|
+
*
|
|
30
|
+
* **Never required, and never blocking.** A project without a domain yet is legitimate — most are on the
|
|
31
|
+
* first day — so an empty answer skips, and a run with no credentials still completes. `pithy init` has
|
|
32
|
+
* never been a command that needs the network, and this does not make it one.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** The zones a picker can offer, or the reason there is no picker. */
|
|
36
|
+
interface ZoneLookup {
|
|
37
|
+
zones: ZoneInfo[];
|
|
38
|
+
/** Said out loud before falling back to free text, so an empty picker is never mistaken for an empty account. */
|
|
39
|
+
note: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read the account's zones, or explain why not.
|
|
44
|
+
*
|
|
45
|
+
* Never throws. Every failure — no credentials, a token without `Zone:Read`, an unreachable account —
|
|
46
|
+
* degrades to free text with a line saying so. Failing the command instead would make declaring a domain
|
|
47
|
+
* impossible offline, which is exactly when someone is scaffolding.
|
|
48
|
+
*/
|
|
49
|
+
async function lookupZones(account: CloudflareAccountSelection | null): Promise<ZoneLookup> {
|
|
50
|
+
const vars = cloudflareEnv({ account });
|
|
51
|
+
const accountId = vars.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
52
|
+
const apiToken = vars.CLOUDFLARE_API_TOKEN ?? "";
|
|
53
|
+
if (!accountId || !apiToken) {
|
|
54
|
+
return { zones: [], note: "No Cloudflare credentials here, so zones cannot be listed. Type the domain instead." };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const zones = await new CloudflareClients({ accountId, apiToken }).zones().listZones();
|
|
59
|
+
if (zones.length === 0) {
|
|
60
|
+
return { zones: [], note: "This account has no zones yet. Type the domain anyway — add the zone before deploy." };
|
|
61
|
+
}
|
|
62
|
+
return { zones, note: null };
|
|
63
|
+
} catch {
|
|
64
|
+
return { zones: [], note: "Could not read this account's zones — the token may lack Zone:Read. Type the domain." };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What `askDomains` decided, for the caller to report and to write. */
|
|
69
|
+
export interface AskedDomains {
|
|
70
|
+
/** The declaration, or undefined when every environment was skipped. */
|
|
71
|
+
domains: WorkerDomains | undefined;
|
|
72
|
+
/** Whether anything was actually asked — a non-interactive run asks nothing. */
|
|
73
|
+
prompted: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Ask where a Worker answers, per environment, against the account's real zones.
|
|
78
|
+
*
|
|
79
|
+
* Returns `{ domains: undefined }` without asking anything when the session is not interactive: every
|
|
80
|
+
* command must work non-interactively with full flags, and a required prompt would break that. An
|
|
81
|
+
* adopter running headless declares `domains` in `pithy.config.ts` directly, which is the same thing this
|
|
82
|
+
* writes.
|
|
83
|
+
*/
|
|
84
|
+
export async function askDomains(options: {
|
|
85
|
+
projectDir: string;
|
|
86
|
+
workerName: string;
|
|
87
|
+
interactive: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* The Cloudflare account whose zones the picker offers. Wrong account, wrong zone list — and a route
|
|
90
|
+
* attached to a zone another account owns fails at deploy with an error naming neither problem.
|
|
91
|
+
*/
|
|
92
|
+
account: CloudflareAccountSelection | null;
|
|
93
|
+
}): Promise<AskedDomains> {
|
|
94
|
+
if (!options.interactive) return { domains: undefined, prompted: false };
|
|
95
|
+
|
|
96
|
+
const { isCancel, note: showNote, select, text } = await import("@clack/prompts");
|
|
97
|
+
const { zones, note } = await lookupZones(options.account);
|
|
98
|
+
if (note) showNote(note);
|
|
99
|
+
|
|
100
|
+
const answers: DomainAnswer[] = [];
|
|
101
|
+
for (const question of domainQuestions(options.workerName)) {
|
|
102
|
+
const hostname = await text({
|
|
103
|
+
message: question.message,
|
|
104
|
+
placeholder: question.placeholder,
|
|
105
|
+
// Empty is a real answer here, so the prompt must not insist. Skipping leaves the resolver to fall
|
|
106
|
+
// through to a route or `BASE_URL`, exactly as it does for a project predating the declaration.
|
|
107
|
+
defaultValue: "",
|
|
108
|
+
});
|
|
109
|
+
if (isCancel(hostname)) {
|
|
110
|
+
process.stderr.write("Canceled.\n");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const value = String(hostname ?? "").trim();
|
|
114
|
+
if (!value) continue;
|
|
115
|
+
|
|
116
|
+
// With zones in hand, confirm the one that owns this hostname rather than inferring it. The
|
|
117
|
+
// inference is right for `api.example.com` and wrong for a nested or multi-part-suffix zone, and
|
|
118
|
+
// getting it wrong produces a route Cloudflare refuses with an error naming neither value.
|
|
119
|
+
let zone: string | undefined;
|
|
120
|
+
if (zones.length > 0) {
|
|
121
|
+
const owning = zoneForHostname(value, zones);
|
|
122
|
+
const choice = await select({
|
|
123
|
+
message: `Zone for ${value}`,
|
|
124
|
+
options: zones.map((candidate) => ({ value: candidate.name, label: describeZone(candidate) })),
|
|
125
|
+
...(owning ? { initialValue: owning.name } : {}),
|
|
126
|
+
});
|
|
127
|
+
if (isCancel(choice)) {
|
|
128
|
+
process.stderr.write("Canceled.\n");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
zone = String(choice);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
answers.push({ env: question.env, hostname: value, ...(zone ? { zone } : {}) });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const built = buildDomains(answers);
|
|
138
|
+
if ("rejected" in built) {
|
|
139
|
+
throw new ValidationError({ message: built.rejected.message, action: built.rejected.action });
|
|
140
|
+
}
|
|
141
|
+
return { domains: built.domains, prompted: true };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Write a declaration into a Worker: the `domains` block in its `pithy.config.ts`, and the `routes` and
|
|
146
|
+
* `vars.BASE_URL` generated from it.
|
|
147
|
+
*
|
|
148
|
+
* Both, always, and in that order. Writing only the declaration would leave a Worker that says where it
|
|
149
|
+
* answers and a `wrangler.jsonc` that does not route there; writing only the generated values would put
|
|
150
|
+
* the truth in the file this whole feature exists to stop hand-maintaining.
|
|
151
|
+
*/
|
|
152
|
+
export async function writeDomains(workerDir: string, domains: WorkerDomains): Promise<{ declared: boolean }> {
|
|
153
|
+
const declared = await writeDomainsDeclaration(workerDir, domains);
|
|
154
|
+
await applyDomains(workerDir, domains);
|
|
155
|
+
return { declared };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The hoisted declaration a scaffolded config carries: `const DOMAINS = {` through its closing `};`. */
|
|
159
|
+
const DOMAINS_CONST = /^const DOMAINS = \{\n(?:.*\n)*?\};$/m;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Write the domains a scaffolded Worker's `pithy.config.ts` declares.
|
|
163
|
+
*
|
|
164
|
+
* **Two shapes, in this order, and the first one is what the scaffold now emits.** A config scaffolded
|
|
165
|
+
* today hoists the declaration above the object —
|
|
166
|
+
*
|
|
167
|
+
* ```ts
|
|
168
|
+
* const DOMAINS = { … };
|
|
169
|
+
* export const PUBLIC_ORIGIN = originFor(compositionEnvironment(), DOMAINS);
|
|
170
|
+
* const config = { domains: DOMAINS, … };
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* — because the origin has to be derivable *before* the capabilities that need it are constructed, and a
|
|
174
|
+
* value nested inside the object literal cannot be read by the same literal (#256). So the writer fills
|
|
175
|
+
* the const, and the `domains: DOMAINS` key beside it is left exactly as it is.
|
|
176
|
+
*
|
|
177
|
+
* Filling the const is not optional politeness. The previous shape inserted a `domains: { … }` key after
|
|
178
|
+
* `const config = {`, guarded only by "does this file already mention a `domains:` key" — and a hoisted
|
|
179
|
+
* config mentions one, `domains: DOMAINS`. So against the scaffold this feature exists to produce, that
|
|
180
|
+
* writer either declined to write at all or, without the guard, wrote a **second** `domains` key into the
|
|
181
|
+
* same object literal, where the last one silently wins.
|
|
182
|
+
*
|
|
183
|
+
* The second shape is the older one: no const, no `domains:` key, so the block goes in after
|
|
184
|
+
* `const config = {`, at the top of the object where an adopter reads it first. That keeps a project
|
|
185
|
+
* scaffolded before this working.
|
|
186
|
+
*
|
|
187
|
+
* A config matching neither is left alone rather than edited blind: returning false lets the caller say
|
|
188
|
+
* the declaration could not be written and print it for the adopter to paste, which is a better outcome
|
|
189
|
+
* than a corrupted config file.
|
|
190
|
+
*/
|
|
191
|
+
async function writeDomainsDeclaration(workerDir: string, domains: WorkerDomains): Promise<boolean> {
|
|
192
|
+
const path = join(workerDir, "pithy.config.ts");
|
|
193
|
+
let source: string;
|
|
194
|
+
try {
|
|
195
|
+
source = await readFile(path, "utf8");
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// The hoisted const wins, and is checked first: a scaffolded config has *both* the const and a
|
|
201
|
+
// `domains:` key, so testing for the key first would send every current project down the older path.
|
|
202
|
+
if (DOMAINS_CONST.test(source)) {
|
|
203
|
+
// A replacement function, so a `$` in a hostname is not read as a capture reference.
|
|
204
|
+
const filled = source.replace(DOMAINS_CONST, () => renderDomainsConst(domains));
|
|
205
|
+
await writeFileAtomic(path, filled);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Line-anchored, not `includes`. A bare substring match also fires on a comment mentioning `domains:`,
|
|
210
|
+
// on a nested key, and on the word inside a string — any of which would silently skip a write the
|
|
211
|
+
// adopter asked for.
|
|
212
|
+
if (/^\s*domains\s*:/m.test(source)) return false;
|
|
213
|
+
const anchor = source.indexOf("const config = {");
|
|
214
|
+
if (anchor === -1) return false;
|
|
215
|
+
const insertAt = source.indexOf("\n", anchor) + 1;
|
|
216
|
+
if (insertAt === 0) return false;
|
|
217
|
+
|
|
218
|
+
await writeFileAtomic(path, `${source.slice(0, insertAt)}${renderDomainsBlock(domains)}\n${source.slice(insertAt)}`);
|
|
219
|
+
return true;
|
|
220
|
+
}
|