@caelo-cms/provisioning 0.10.46 → 0.10.48
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/dist/adc-impersonation.d.ts.map +1 -1
- package/dist/adc-impersonation.js +23 -1
- package/dist/adc-impersonation.js.map +1 -1
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18362 -0
- package/dist/cli.js +122 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +19 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +31 -3
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -5
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/state-attach.d.ts +23 -0
- package/dist/state-attach.d.ts.map +1 -0
- package/dist/state-attach.js +112 -0
- package/dist/state-attach.js.map +1 -0
- package/dist/state-backend.d.ts +11 -0
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +20 -0
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +20 -0
- package/dist/state-command.d.ts.map +1 -0
- package/dist/state-command.js +237 -0
- package/dist/state-command.js.map +1 -0
- package/dist/state-migrate.d.ts +121 -0
- package/dist/state-migrate.d.ts.map +1 -0
- package/dist/state-migrate.js +290 -0
- package/dist/state-migrate.js.map +1 -0
- package/dist/state-pulumi.d.ts +17 -0
- package/dist/state-pulumi.d.ts.map +1 -0
- package/dist/state-pulumi.js +218 -0
- package/dist/state-pulumi.js.map +1 -0
- package/dist/state-store.d.ts +27 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +65 -0
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +131 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +9 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +8 -6
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +291 -83
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +382 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +293 -0
- package/src/release-manifest.ts +430 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +391 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
package/src/wizard.ts
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bunx @caelo-cms/provisioning` — interactive wizard.
|
|
5
|
+
*
|
|
6
|
+
* Per CLAUDE.md §11.C, this is the primary surface OSS users interact
|
|
7
|
+
* with: one command, end-to-end, < 20 min, on the user's own cloud.
|
|
8
|
+
*
|
|
9
|
+
* Top-level shape:
|
|
10
|
+
* 1. Pick provider (gcp / aws / azure / self-hosted)
|
|
11
|
+
* 2. Detect existing install at `~/.caelo-<id>/` and offer to resume
|
|
12
|
+
* 3. Prompt for the few inputs the provider needs (domain, owner email,
|
|
13
|
+
* region — detect-then-confirm, fixed after install (#607),
|
|
14
|
+
* project id, Anthropic key — input-hidden)
|
|
15
|
+
* 4. Show cost estimate + final confirm
|
|
16
|
+
* 5. Delegate to the provider-specific wizard which handles project
|
|
17
|
+
* bootstrap → Pulumi up → DNS → cert → IAP enable → bootstrap URL
|
|
18
|
+
*
|
|
19
|
+
* Each provider wizard is a separate file under `wizards/<provider>.ts`
|
|
20
|
+
* so adding a new provider is one new file, not a sprawling switch.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { defaultStagingHost, normalizeStagingHost, stagingHostProblem } from "@caelo-cms/shared";
|
|
24
|
+
import {
|
|
25
|
+
cancel,
|
|
26
|
+
confirm,
|
|
27
|
+
intro,
|
|
28
|
+
isCancel,
|
|
29
|
+
log,
|
|
30
|
+
outro,
|
|
31
|
+
select,
|
|
32
|
+
spinner,
|
|
33
|
+
text,
|
|
34
|
+
} from "@clack/prompts";
|
|
35
|
+
import { bold, cyan, dim, yellow } from "kleur/colors";
|
|
36
|
+
import {
|
|
37
|
+
currentMetadata,
|
|
38
|
+
deriveInstallId,
|
|
39
|
+
ensureInstallDir,
|
|
40
|
+
type InstallMetadata,
|
|
41
|
+
listInstalls,
|
|
42
|
+
type Provider,
|
|
43
|
+
readMetadata,
|
|
44
|
+
resumedMetadata,
|
|
45
|
+
scrubInstallProgress,
|
|
46
|
+
writeMetadata,
|
|
47
|
+
} from "./install-state.js";
|
|
48
|
+
import type { RunMode } from "./run-mode.js";
|
|
49
|
+
import { selfCommand } from "./self-command.js";
|
|
50
|
+
import { runGcpWizard } from "./wizards/gcp.js";
|
|
51
|
+
import { runGcpFirebaseWizard } from "./wizards/gcp-firebase.js";
|
|
52
|
+
|
|
53
|
+
export interface WizardOptions {
|
|
54
|
+
/** Skip prompts; require all inputs via flags. CI-friendly. */
|
|
55
|
+
nonInteractive?: boolean;
|
|
56
|
+
/** Pre-supplied provider — skips the picker. */
|
|
57
|
+
provider?: Provider;
|
|
58
|
+
/** Pre-supplied domain — skips the prompt. */
|
|
59
|
+
domain?: string;
|
|
60
|
+
/**
|
|
61
|
+
* #650 — `--staging-domain`: the protected staging site's host; skips the
|
|
62
|
+
* prompt. Default `staging.<domain>`.
|
|
63
|
+
*/
|
|
64
|
+
stagingDomain?: string;
|
|
65
|
+
/** Pre-supplied owner email — skips the prompt. */
|
|
66
|
+
ownerEmail?: string;
|
|
67
|
+
/** Pre-supplied GCP project id — skips the prompt for gcp. */
|
|
68
|
+
projectId?: string;
|
|
69
|
+
/**
|
|
70
|
+
* #607 — cloud region for a new install; skips the prompt. Required with
|
|
71
|
+
* `nonInteractive` for a new install. Fixed after install: on an existing
|
|
72
|
+
* install a different value is refused.
|
|
73
|
+
*/
|
|
74
|
+
region?: string;
|
|
75
|
+
/**
|
|
76
|
+
* #696 — where `pulumi up` and the rest of a GCP install run
|
|
77
|
+
* (run-mode.ts): `job` (default in Cloud Shell) or `local`.
|
|
78
|
+
*/
|
|
79
|
+
runMode?: RunMode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runWizard(opts: WizardOptions = {}): Promise<void> {
|
|
83
|
+
intro(bold(cyan("Caelo CMS provisioner")) + dim(" — one-command deploy"));
|
|
84
|
+
|
|
85
|
+
// 1. Resume-existing-install fast path. Before prompting for anything,
|
|
86
|
+
// scan ~/.caelo-* for installs and offer them as "resume X" options.
|
|
87
|
+
// Picking an existing install carries domain + owner + projectId
|
|
88
|
+
// over and skips re-prompting; "new install" falls through to the
|
|
89
|
+
// fresh-flow below.
|
|
90
|
+
//
|
|
91
|
+
// v0.3.18 — previously the wizard re-asked for provider + domain +
|
|
92
|
+
// owner-email + project EVERY re-run. With this fast path, a
|
|
93
|
+
// re-run is a single Enter to "resume <id>".
|
|
94
|
+
let provider: Provider;
|
|
95
|
+
let domain: string;
|
|
96
|
+
let ownerEmail: string;
|
|
97
|
+
let projectIdHint: string | null;
|
|
98
|
+
let installId: string;
|
|
99
|
+
|
|
100
|
+
// #696 — job mode (Cloud Shell): the project comes first and is the
|
|
101
|
+
// source of truth; this machine's ~/.caelo-* is not consulted.
|
|
102
|
+
const runMode = opts.runMode ?? "local";
|
|
103
|
+
let jobModeProjectId: string | null = null;
|
|
104
|
+
let jobModeExisting: InstallMetadata | null = null;
|
|
105
|
+
let jobModeProvider: Provider | null = null;
|
|
106
|
+
if (runMode === "job") {
|
|
107
|
+
jobModeProvider = opts.provider ?? (await pickProvider());
|
|
108
|
+
if (jobModeProvider !== "gcp" && jobModeProvider !== "gcp-firebase") {
|
|
109
|
+
cancel(
|
|
110
|
+
`The install job runs gcp and gcp-firebase installs; for ${jobModeProvider} run the installer with --local.`,
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const { prepareJobModeInstall } = await import("./wizards/gcp-install-job.js");
|
|
115
|
+
const prep = await prepareJobModeInstall(opts.projectId ? { projectId: opts.projectId } : {});
|
|
116
|
+
if (prep.kind === "handled") {
|
|
117
|
+
outro(bold("Done."));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (prep.kind === "existing") jobModeExisting = prep.meta;
|
|
121
|
+
else jobModeProjectId = prep.projectId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const existing = opts.nonInteractive || runMode === "job" ? [] : listInstalls();
|
|
125
|
+
const picked =
|
|
126
|
+
existing.length > 0 && !opts.provider && !opts.domain && !opts.ownerEmail
|
|
127
|
+
? await pickInstallToResume(existing)
|
|
128
|
+
: null;
|
|
129
|
+
// #696 — the bucket's copy for an install whose state lives there: another
|
|
130
|
+
// machine may have changed its domain or owner since this mirror was written.
|
|
131
|
+
const resumeChoice = jobModeExisting ?? (picked ? currentMetadata(picked) : null);
|
|
132
|
+
if (jobModeExisting && jobModeProvider && jobModeExisting.provider !== jobModeProvider) {
|
|
133
|
+
log.warn(
|
|
134
|
+
yellow(
|
|
135
|
+
`This project's install is ${jobModeExisting.provider}, not ${jobModeProvider}; continuing it as ${jobModeExisting.provider}.`,
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (resumeChoice) {
|
|
141
|
+
provider = resumeChoice.provider;
|
|
142
|
+
domain = resumeChoice.domain;
|
|
143
|
+
ownerEmail = resumeChoice.ownerEmail;
|
|
144
|
+
projectIdHint = resumeChoice.projectId;
|
|
145
|
+
installId = resumeChoice.installId;
|
|
146
|
+
ensureInstallDir(installId);
|
|
147
|
+
} else {
|
|
148
|
+
provider = jobModeProvider ?? opts.provider ?? (await pickProvider());
|
|
149
|
+
domain = opts.domain ?? (await promptDomain());
|
|
150
|
+
const stagingDomain = await resolveStagingDomain(domain, provider, opts);
|
|
151
|
+
ownerEmail = opts.ownerEmail ?? (await promptOwnerEmail());
|
|
152
|
+
projectIdHint = jobModeProjectId ?? opts.projectId ?? null;
|
|
153
|
+
installId = deriveInstallId(provider, projectIdHint ?? domain);
|
|
154
|
+
ensureInstallDir(installId);
|
|
155
|
+
|
|
156
|
+
// If the user picked "new install" but the derived id collides with
|
|
157
|
+
// an existing one (e.g. retyped the same domain), confirm resume.
|
|
158
|
+
const existingForId = readMetadata(installId);
|
|
159
|
+
if (existingForId && !opts.nonInteractive) {
|
|
160
|
+
const resume = await confirm({
|
|
161
|
+
message: `Existing install '${installId}' detected (created ${dim(existingForId.createdAt)}). Resume?`,
|
|
162
|
+
initialValue: true,
|
|
163
|
+
});
|
|
164
|
+
if (isCancel(resume)) {
|
|
165
|
+
cancel("Cancelled.");
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
if (!resume) {
|
|
169
|
+
cancel(`Aborted. Run with a different domain or remove ~/.caelo-${installId}/ first.`);
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
} else if (existingForId) {
|
|
173
|
+
// Non-interactive re-run of an existing install: resume it. Keep its
|
|
174
|
+
// metadata (createdAt, region, the recorded release in imageDigests)
|
|
175
|
+
// so the re-run converges infrastructure instead of looking like a
|
|
176
|
+
// new install that deploys `:latest`.
|
|
177
|
+
writeMetadata(installId, {
|
|
178
|
+
...resumedMetadata(existingForId, { domain, ownerEmail, projectId: projectIdHint }),
|
|
179
|
+
...(opts.stagingDomain ? { stagingDomain } : {}),
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
writeMetadata(installId, {
|
|
183
|
+
installId,
|
|
184
|
+
provider,
|
|
185
|
+
projectId: projectIdHint,
|
|
186
|
+
domain,
|
|
187
|
+
stagingDomain,
|
|
188
|
+
ownerEmail,
|
|
189
|
+
region: null,
|
|
190
|
+
createdAt: new Date().toISOString(),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 3.5. #656 — a re-run of an install an earlier CLI set up: drop the
|
|
196
|
+
// secrets that CLI checkpointed (database passwords, the setup link)
|
|
197
|
+
// before any step writes progress.json again, and say what to rotate.
|
|
198
|
+
for (const line of scrubInstallProgress(installId)) log.warn(yellow(line));
|
|
199
|
+
|
|
200
|
+
// 4. Delegate to the provider-specific wizard.
|
|
201
|
+
switch (provider) {
|
|
202
|
+
case "gcp":
|
|
203
|
+
await runGcpWizard({
|
|
204
|
+
installId,
|
|
205
|
+
domain,
|
|
206
|
+
ownerEmail,
|
|
207
|
+
projectId: projectIdHint,
|
|
208
|
+
nonInteractive: opts.nonInteractive ?? false,
|
|
209
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
210
|
+
runMode,
|
|
211
|
+
});
|
|
212
|
+
break;
|
|
213
|
+
case "gcp-firebase":
|
|
214
|
+
await runGcpFirebaseWizard({
|
|
215
|
+
installId,
|
|
216
|
+
domain,
|
|
217
|
+
ownerEmail,
|
|
218
|
+
projectId: projectIdHint,
|
|
219
|
+
nonInteractive: opts.nonInteractive ?? false,
|
|
220
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
221
|
+
runMode,
|
|
222
|
+
});
|
|
223
|
+
break;
|
|
224
|
+
case "self-hosted":
|
|
225
|
+
cancel(
|
|
226
|
+
`Self-hosted wizard is the existing \`cms-provision init\` flow. Run \`${selfCommand("init", "--domain", "...", "--owner-email", "...")}\` for now; wizard polish lands in a follow-up commit.`,
|
|
227
|
+
);
|
|
228
|
+
process.exit(0);
|
|
229
|
+
break;
|
|
230
|
+
case "aws":
|
|
231
|
+
case "azure":
|
|
232
|
+
cancel(
|
|
233
|
+
`${provider.toUpperCase()} provider wizard not yet implemented. GCP is the first cloud target; AWS + Azure follow the same shape and land in upcoming commits.`,
|
|
234
|
+
);
|
|
235
|
+
process.exit(0);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
outro(bold("Done. Welcome to Caelo CMS."));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Offer the operator the list of existing installs (newest first)
|
|
244
|
+
* plus a "start a new install" option. Returning `null` means the
|
|
245
|
+
* operator picked "new" and the wizard falls through to the fresh-
|
|
246
|
+
* inputs prompts. Returning an install means resume — every input
|
|
247
|
+
* (provider, domain, owner, project) carries over from metadata.
|
|
248
|
+
*/
|
|
249
|
+
async function pickInstallToResume(installs: InstallMetadata[]): Promise<InstallMetadata | null> {
|
|
250
|
+
const first = installs[0];
|
|
251
|
+
// v0.5.14 — added per-install "Upgrade" entries that route to
|
|
252
|
+
// upgradeCommand (Cloud Run image roll) without re-running the
|
|
253
|
+
// wizard. Operator complaint: running bare `bunx @caelo-cms/
|
|
254
|
+
// provisioning@latest` on a working install dropped them into
|
|
255
|
+
// the wizard and they assumed it would upgrade. It didn't — the
|
|
256
|
+
// wizard provisions, only `upgrade` rolls a running deployment.
|
|
257
|
+
// Surfacing both intents in the same picker closes the gap.
|
|
258
|
+
const choice = await select<string>({
|
|
259
|
+
message:
|
|
260
|
+
installs.length === 1 && first
|
|
261
|
+
? `What do you want to do with '${first.installId}'?`
|
|
262
|
+
: "Pick an existing install (or start a new one):",
|
|
263
|
+
options: [
|
|
264
|
+
...installs.flatMap((m) => [
|
|
265
|
+
{
|
|
266
|
+
value: `upgrade:${m.installId}`,
|
|
267
|
+
label: `Upgrade ${bold(m.installId)} ${dim(`(${m.provider}, ${m.domain})`)}`,
|
|
268
|
+
hint: `roll Cloud Run to the latest image — does not re-run the wizard`,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
value: `resume:${m.installId}`,
|
|
272
|
+
label: `Resume wizard for ${bold(m.installId)} ${dim(`(${m.provider}, ${m.domain})`)}`,
|
|
273
|
+
hint: `created ${m.createdAt.slice(0, 10)} — re-runs provisioning steps`,
|
|
274
|
+
},
|
|
275
|
+
]),
|
|
276
|
+
{ value: "__new__", label: "Start a new install", hint: "fresh inputs" },
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
if (isCancel(choice)) {
|
|
280
|
+
cancel("Cancelled.");
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
if (choice === "__new__") return null;
|
|
284
|
+
// v0.5.14 — upgrade short-circuit. Dispatch to upgradeCommand
|
|
285
|
+
// directly and exit; we don't fall through to runWizard's main flow.
|
|
286
|
+
if (choice.startsWith("upgrade:")) {
|
|
287
|
+
const installId = choice.slice("upgrade:".length);
|
|
288
|
+
const target = installs.find((m) => m.installId === installId);
|
|
289
|
+
if (!target) {
|
|
290
|
+
cancel(`Could not find install metadata for '${installId}'.`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
const { upgradeCommand } = await import("./lifecycle.js");
|
|
294
|
+
// Defaults to channel=stable / tag=:latest — explicit-version is
|
|
295
|
+
// available via `cms-provision upgrade --version vX.Y.Z`. The picked
|
|
296
|
+
// install is passed on: without it upgrade would choose again (and,
|
|
297
|
+
// before that was fixed, took the first ~/.caelo-* it found).
|
|
298
|
+
await upgradeCommand({ installId: target.installId });
|
|
299
|
+
process.exit(0);
|
|
300
|
+
}
|
|
301
|
+
const installId = choice.startsWith("resume:") ? choice.slice("resume:".length) : choice;
|
|
302
|
+
return installs.find((m) => m.installId === installId) ?? null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function pickProvider(): Promise<Provider> {
|
|
306
|
+
const choice = await select<Provider>({
|
|
307
|
+
message: "Where do you want to deploy?",
|
|
308
|
+
options: [
|
|
309
|
+
{
|
|
310
|
+
value: "gcp",
|
|
311
|
+
label: "Google Cloud Platform",
|
|
312
|
+
hint: "Cloud Run + Cloud SQL + Cloud Storage + Cloud CDN. Single LB serves admin + gateway + static.",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
value: "gcp-firebase",
|
|
316
|
+
label: "Google Cloud Platform (Firebase Hosting)",
|
|
317
|
+
hint: "Cloud Run + Cloud SQL + Firebase Hosting (no LB). Lower fixed cost, native clean URLs + preview channels. Gateway via Firebase rewrites (no Cloud Armor).",
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
value: "self-hosted",
|
|
321
|
+
label: "Self-hosted (Docker Compose)",
|
|
322
|
+
hint: "Single Linux box. Postgres + Caddy + the admin + the gateway.",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
value: "aws",
|
|
326
|
+
label: "Amazon Web Services",
|
|
327
|
+
hint: "Lambda + RDS + S3 + CloudFront. Lands in a follow-up commit.",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
value: "azure",
|
|
331
|
+
label: "Microsoft Azure",
|
|
332
|
+
hint: "Container Apps + Azure DB + Blob + Front Door. Lands in a follow-up commit.",
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
});
|
|
336
|
+
if (isCancel(choice)) {
|
|
337
|
+
cancel("Cancelled.");
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
return choice as Provider;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function promptDomain(): Promise<string> {
|
|
344
|
+
const value = await text({
|
|
345
|
+
message: "Domain you'd like to use",
|
|
346
|
+
placeholder: "mysite.com",
|
|
347
|
+
validate: (v) => {
|
|
348
|
+
if (!v || v.length === 0) return "Domain is required";
|
|
349
|
+
if (
|
|
350
|
+
!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/.test(v)
|
|
351
|
+
) {
|
|
352
|
+
return "Looks like an invalid domain (e.g. mysite.com)";
|
|
353
|
+
}
|
|
354
|
+
return undefined;
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
if (isCancel(value)) {
|
|
358
|
+
cancel("Cancelled.");
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
return value as string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* #650 — the protected staging site's host: `--staging-domain`, else asked
|
|
366
|
+
* (default `staging.<domain>`; any other FQDN, also on another domain),
|
|
367
|
+
* else — non-interactive — the default. Never the live or admin host.
|
|
368
|
+
*/
|
|
369
|
+
async function resolveStagingDomain(
|
|
370
|
+
domain: string,
|
|
371
|
+
provider: Provider,
|
|
372
|
+
opts: Pick<WizardOptions, "stagingDomain" | "nonInteractive">,
|
|
373
|
+
): Promise<string> {
|
|
374
|
+
const reserved = { liveHosts: [domain, `www.${domain}`], adminHosts: [`admin.${domain}`] };
|
|
375
|
+
const problemOf = (host: string) => stagingHostProblem(host, reserved, { provider });
|
|
376
|
+
if (opts.stagingDomain) {
|
|
377
|
+
const problem = problemOf(opts.stagingDomain);
|
|
378
|
+
if (problem) {
|
|
379
|
+
cancel(`--staging-domain: ${problem}`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
return normalizeStagingHost(opts.stagingDomain);
|
|
383
|
+
}
|
|
384
|
+
const suggested = defaultStagingHost(domain);
|
|
385
|
+
if (opts.nonInteractive) {
|
|
386
|
+
// The default can be too long for the provider (gcp-firebase: 64).
|
|
387
|
+
const problem = problemOf(suggested);
|
|
388
|
+
if (problem) {
|
|
389
|
+
cancel(`the default staging host ${problem}. Pass --staging-domain <host>.`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
return suggested;
|
|
393
|
+
}
|
|
394
|
+
const value = await text({
|
|
395
|
+
message: "Host of the protected staging site (where you review changes before they go live)",
|
|
396
|
+
placeholder: suggested,
|
|
397
|
+
initialValue: suggested,
|
|
398
|
+
validate: (v) => problemOf(v || suggested) ?? undefined,
|
|
399
|
+
});
|
|
400
|
+
if (isCancel(value)) {
|
|
401
|
+
cancel("Cancelled.");
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
return normalizeStagingHost((value as string) || suggested);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function promptOwnerEmail(): Promise<string> {
|
|
408
|
+
const value = await text({
|
|
409
|
+
message: "Owner email (you'll be the IAP allowlisted Owner; can add more later)",
|
|
410
|
+
placeholder: "you@example.com",
|
|
411
|
+
validate: (v) => {
|
|
412
|
+
if (!v || v.length === 0) return "Email is required";
|
|
413
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return "Looks like an invalid email";
|
|
414
|
+
return undefined;
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
if (isCancel(value)) {
|
|
418
|
+
cancel("Cancelled.");
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
return value as string;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Re-export the spinner factory so per-provider wizards use a consistent UX.
|
|
425
|
+
export { spinner };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-resource static price table for the GCP stack — used by the
|
|
5
|
+
* wizard's pre-flight cost estimate. Prices are EUR/USD per month at
|
|
6
|
+
* `europe-west1` list rates as of 2026-05; ship updates with each
|
|
7
|
+
* Caelo release.
|
|
8
|
+
*
|
|
9
|
+
* The estimate is intentionally OVER-conservative — it shows the
|
|
10
|
+
* floor cost (idle + scale-to-zero traffic). Real usage adds egress
|
|
11
|
+
* + AI calls + storage growth on top.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ADMIN_MEMORY_DEFAULT, memoryQuantityMiB } from "../stack-contract.js";
|
|
15
|
+
|
|
16
|
+
export interface CostLine {
|
|
17
|
+
name: string;
|
|
18
|
+
monthlyUsd: number;
|
|
19
|
+
notes?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CostEstimateInputs {
|
|
23
|
+
cloudSqlTier: string; // e.g. "db-f1-micro"
|
|
24
|
+
cloudSqlHa: boolean;
|
|
25
|
+
adminMinInstances: number;
|
|
26
|
+
/**
|
|
27
|
+
* #553 — admin memory (`2Gi`). Cloud Run bills memory only while an
|
|
28
|
+
* instance runs, so it costs nothing extra at scale-to-zero and
|
|
29
|
+
* ~$6.50/mo per GiB above 1 GiB for each always-on min instance.
|
|
30
|
+
*/
|
|
31
|
+
adminMemory?: string;
|
|
32
|
+
gatewayMinInstances: number;
|
|
33
|
+
wafAdaptiveProtection: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* v0.3.3 — provider variant. 'gcp' is the LB-fronted topology
|
|
36
|
+
* (+$18 LB + Cloud CDN + Cloud Armor lines). 'gcp-firebase'
|
|
37
|
+
* drops all three (no LB, Firebase Hosting native CDN, no Cloud
|
|
38
|
+
* Armor) — saves ~$19/mo. Defaults to 'gcp' for backwards
|
|
39
|
+
* compatibility.
|
|
40
|
+
*/
|
|
41
|
+
provider?: "gcp" | "gcp-firebase";
|
|
42
|
+
/** #607 — the install region; shown in the table (prices are {@link PRICED_REGION} rates). */
|
|
43
|
+
region: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The region the price table's list rates are for. */
|
|
47
|
+
const PRICED_REGION = "europe-west1";
|
|
48
|
+
|
|
49
|
+
const SQL_TIER_USD: Record<string, number> = {
|
|
50
|
+
// Shared-core legacy tiers, ENTERPRISE edition
|
|
51
|
+
"db-f1-micro": 9.5,
|
|
52
|
+
"db-g1-small": 30,
|
|
53
|
+
// Per-N tiers (ENTERPRISE_PLUS only)
|
|
54
|
+
"db-perf-optimized-N-2": 50,
|
|
55
|
+
"db-perf-optimized-N-4": 95,
|
|
56
|
+
"db-perf-optimized-N-8": 180,
|
|
57
|
+
// Custom-machine fallback (rough)
|
|
58
|
+
"db-custom-1-3840": 35,
|
|
59
|
+
"db-custom-2-7680": 65,
|
|
60
|
+
"db-custom-4-15360": 130,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
64
|
+
lines: CostLine[];
|
|
65
|
+
totalUsd: number;
|
|
66
|
+
/** The region line the pre-flight table shows above the costs. */
|
|
67
|
+
regionNote: string;
|
|
68
|
+
} {
|
|
69
|
+
const sqlBase = SQL_TIER_USD[inputs.cloudSqlTier] ?? 30;
|
|
70
|
+
const sqlMultiplier = inputs.cloudSqlHa ? 2.0 : 1.0;
|
|
71
|
+
const sqlMonthly = Math.round(sqlBase * sqlMultiplier);
|
|
72
|
+
|
|
73
|
+
const provider = inputs.provider ?? "gcp";
|
|
74
|
+
const adminMemory = inputs.adminMemory ?? ADMIN_MEMORY_DEFAULT;
|
|
75
|
+
const adminMiB = memoryQuantityMiB(adminMemory);
|
|
76
|
+
if (adminMiB === null) {
|
|
77
|
+
throw new Error(`adminMemory "${adminMemory}" is not a memory quantity (e.g. 2Gi)`);
|
|
78
|
+
}
|
|
79
|
+
const adminGiB = adminMiB / 1024;
|
|
80
|
+
// An always-on instance is priced at 1 GiB (the $15 line); extra memory
|
|
81
|
+
// is billed per GiB-second (~$6.50 per GiB-month).
|
|
82
|
+
const perMinInstanceUsd = 15 + Math.max(0, adminGiB - 1) * 6.5;
|
|
83
|
+
|
|
84
|
+
const lines: CostLine[] = [
|
|
85
|
+
{
|
|
86
|
+
name: `Cloud SQL Postgres (${inputs.cloudSqlTier}${inputs.cloudSqlHa ? ", HA" : ""})`,
|
|
87
|
+
monthlyUsd: sqlMonthly,
|
|
88
|
+
notes: inputs.cloudSqlHa
|
|
89
|
+
? "REGIONAL availability (synchronous replica)"
|
|
90
|
+
: "ZONAL — single zone, automated backups",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: `Cloud Run admin (${adminMemory})`,
|
|
94
|
+
monthlyUsd:
|
|
95
|
+
inputs.adminMinInstances === 0
|
|
96
|
+
? 1
|
|
97
|
+
: Math.round(1 + inputs.adminMinInstances * perMinInstanceUsd),
|
|
98
|
+
notes:
|
|
99
|
+
inputs.adminMinInstances === 0
|
|
100
|
+
? "scale-to-zero; ~$1/mo light editorial use"
|
|
101
|
+
: `${inputs.adminMinInstances} min-instance${inputs.adminMinInstances > 1 ? "s" : ""} (no cold start)`,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "Quality checks (Lighthouse on the admin)",
|
|
105
|
+
monthlyUsd: 1,
|
|
106
|
+
notes:
|
|
107
|
+
"~1-2 min of admin CPU per audited Stage (up to 5 pages x 3 runs); ~$1/mo at a few Stages a day",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "Cloud Run gateway",
|
|
111
|
+
monthlyUsd: inputs.gatewayMinInstances === 0 ? 1 : 1 + inputs.gatewayMinInstances * 15,
|
|
112
|
+
notes:
|
|
113
|
+
inputs.gatewayMinInstances === 0
|
|
114
|
+
? "scale-to-zero; ~$1/mo light traffic"
|
|
115
|
+
: `${inputs.gatewayMinInstances} min-instance${inputs.gatewayMinInstances > 1 ? "s" : ""}`,
|
|
116
|
+
},
|
|
117
|
+
// v0.3.3 — LB / Cloud CDN / Cloud Armor lines apply only to the
|
|
118
|
+
// gcp variant. gcp-firebase has no LB (Firebase Hosting is the
|
|
119
|
+
// edge); no Cloud CDN line (Firebase Hosting has its own CDN
|
|
120
|
+
// included in free tier); no Cloud Armor (admin is gated by
|
|
121
|
+
// Cloud Run IAP, gateway by run.invoker IAM).
|
|
122
|
+
...(provider === "gcp"
|
|
123
|
+
? ([
|
|
124
|
+
{
|
|
125
|
+
name: "Load balancer + managed SSL cert",
|
|
126
|
+
monthlyUsd: 18,
|
|
127
|
+
notes: "Global LB base fee + cert (free) — flat",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "Cloud CDN cache",
|
|
131
|
+
monthlyUsd: 1,
|
|
132
|
+
notes: "Free egress at edge cache hits",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "Cloud Armor WAF",
|
|
136
|
+
monthlyUsd: inputs.wafAdaptiveProtection ? 5 : 0,
|
|
137
|
+
notes: inputs.wafAdaptiveProtection
|
|
138
|
+
? "Adaptive protection (ML-based bot mitigation)"
|
|
139
|
+
: "Free tier — rate limit + OWASP basic rules",
|
|
140
|
+
},
|
|
141
|
+
] satisfies CostLine[])
|
|
142
|
+
: ([
|
|
143
|
+
{
|
|
144
|
+
name: "Firebase Hosting",
|
|
145
|
+
monthlyUsd: 0,
|
|
146
|
+
notes: "Free tier: 10 GB storage + 360 MB/day egress",
|
|
147
|
+
},
|
|
148
|
+
] satisfies CostLine[])),
|
|
149
|
+
{
|
|
150
|
+
name:
|
|
151
|
+
provider === "gcp"
|
|
152
|
+
? "Cloud Storage (static + media)"
|
|
153
|
+
: "Cloud Storage (media only — static lives on Firebase)",
|
|
154
|
+
monthlyUsd: 1,
|
|
155
|
+
notes: "~5 GB storage + low egress; growth roughly $0.02/GB",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "Secret Manager (5 secrets)",
|
|
159
|
+
monthlyUsd: 0,
|
|
160
|
+
notes: "Free tier",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "BigQuery edge log sink",
|
|
164
|
+
monthlyUsd: 0,
|
|
165
|
+
notes: "Free tier — pay only on >1 TB/mo query",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "Install state (ops bucket + Cloud KMS key)",
|
|
169
|
+
monthlyUsd: 0.06,
|
|
170
|
+
notes:
|
|
171
|
+
"< $0.10/mo — versioned bucket with Pulumi state + install metadata, 1 KMS key version ($0.06)",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "Operator-access sync (Cloud Run job + Cloud Scheduler)",
|
|
175
|
+
monthlyUsd: 0,
|
|
176
|
+
notes:
|
|
177
|
+
"Hourly ~20 s job keeps Google IAP in step with the user list; within the Cloud Run free tier, 1 of 3 free scheduler jobs",
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
// Cents, so a sub-dollar line (the KMS key) doesn't print float noise.
|
|
182
|
+
const totalUsd = Math.round(lines.reduce((sum, l) => sum + l.monthlyUsd, 0) * 100) / 100;
|
|
183
|
+
const regionNote =
|
|
184
|
+
inputs.region === PRICED_REGION
|
|
185
|
+
? `Region: ${inputs.region} (fixed after install)`
|
|
186
|
+
: `Region: ${inputs.region} (fixed after install) — prices below are ${PRICED_REGION} list rates; ${inputs.region} rates may differ`;
|
|
187
|
+
return { lines, totalUsd, regionNote };
|
|
188
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* v0.3.0 — GCP Firebase Hosting provider wizard.
|
|
5
|
+
*
|
|
6
|
+
* Architectural differences from the `gcp` wizard:
|
|
7
|
+
* - No load balancer / url-map / TLS cert / BackendBucket
|
|
8
|
+
* - Static site → Firebase Hosting (apex domain)
|
|
9
|
+
* - admin.<domain> → Cloud Run direct + IAP-on-Cloud-Run
|
|
10
|
+
* - <domain>/api/* → Firebase Hosting `rewrites` → Cloud Run gateway
|
|
11
|
+
*
|
|
12
|
+
* The Pulumi stack at packages/provisioning/stacks/gcp-firebase/
|
|
13
|
+
* provisions all of the above. The wizard's pre-Pulumi work
|
|
14
|
+
* (gcloud auth, project, billing, APIs, SA, secrets, anthropic-api-key
|
|
15
|
+
* capture) is structurally identical to the `gcp` wizard — same
|
|
16
|
+
* helpers, same flow — only the final Pulumi invocation differs.
|
|
17
|
+
*
|
|
18
|
+
* For v0.3.0 the wizard delegates to the same gcp pre-Pulumi steps
|
|
19
|
+
* via the shared `runGcpWizard` flow and only swaps the Pulumi
|
|
20
|
+
* runtime target. If `runGcpWizard`'s flow needs to diverge further
|
|
21
|
+
* (e.g. Firebase API enablement, Firebase project initialization),
|
|
22
|
+
* we'll split it. Until then the duplication is one fn call.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { log, note } from "@clack/prompts";
|
|
26
|
+
import { bold, cyan } from "kleur/colors";
|
|
27
|
+
import type { RunMode } from "../run-mode.js";
|
|
28
|
+
|
|
29
|
+
export interface GcpFirebaseWizardOpts {
|
|
30
|
+
installId: string;
|
|
31
|
+
domain: string;
|
|
32
|
+
ownerEmail: string;
|
|
33
|
+
projectId: string | null;
|
|
34
|
+
nonInteractive: boolean;
|
|
35
|
+
/** #607 — `--region`; see {@link GcpWizardOpts.region}. */
|
|
36
|
+
region?: string;
|
|
37
|
+
/** #696 — see {@link GcpWizardOpts.runMode}. */
|
|
38
|
+
runMode?: RunMode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runGcpFirebaseWizard(opts: GcpFirebaseWizardOpts): Promise<void> {
|
|
42
|
+
log.step(bold("GCP Firebase Hosting provider"));
|
|
43
|
+
note(
|
|
44
|
+
[
|
|
45
|
+
"This provider variant uses:",
|
|
46
|
+
` ${cyan("Firebase Hosting")} for the static site (clean URLs + native preview channels)`,
|
|
47
|
+
` ${cyan("Cloud Run direct")} for admin (admin.<domain>, IAP-on-Cloud-Run)`,
|
|
48
|
+
` ${cyan("Firebase rewrites")} for the gateway (no LB, no Cloud Armor)`,
|
|
49
|
+
"",
|
|
50
|
+
"Lower fixed cost than the `gcp` provider (no LB ~$18/mo savings).",
|
|
51
|
+
"Trade-off: no Cloud Armor on the gateway — in-app rate limits + Firebase Edge DDoS only.",
|
|
52
|
+
].join("\n"),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// v0.3.1 — the gcloud pre-Pulumi steps are structurally identical
|
|
56
|
+
// (project, billing, APIs, SA, secrets capture) and the shared
|
|
57
|
+
// `runGcpWizard` flow now accepts a `provider` arg that:
|
|
58
|
+
// - enables the additional Firebase APIs in stepEnableApis
|
|
59
|
+
// - routes the final pulumiUpGcp at stacks/gcp-firebase
|
|
60
|
+
// - uses the caelo-gcp-firebase config namespace
|
|
61
|
+
const { runGcpWizard } = await import("./gcp.js");
|
|
62
|
+
await runGcpWizard({
|
|
63
|
+
installId: opts.installId,
|
|
64
|
+
domain: opts.domain,
|
|
65
|
+
ownerEmail: opts.ownerEmail,
|
|
66
|
+
projectId: opts.projectId,
|
|
67
|
+
nonInteractive: opts.nonInteractive,
|
|
68
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
69
|
+
...(opts.runMode ? { runMode: opts.runMode } : {}),
|
|
70
|
+
provider: "gcp-firebase",
|
|
71
|
+
});
|
|
72
|
+
}
|