@caelo-cms/provisioning 0.10.47 → 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.
Files changed (315) hide show
  1. package/dist/agent/apply.d.ts +45 -0
  2. package/dist/agent/apply.d.ts.map +1 -0
  3. package/dist/agent/apply.js +83 -0
  4. package/dist/agent/apply.js.map +1 -0
  5. package/dist/agent/cli-provenance.d.ts +42 -0
  6. package/dist/agent/cli-provenance.d.ts.map +1 -0
  7. package/dist/agent/cli-provenance.js +138 -0
  8. package/dist/agent/cli-provenance.js.map +1 -0
  9. package/dist/agent/command.d.ts +79 -0
  10. package/dist/agent/command.d.ts.map +1 -0
  11. package/dist/agent/command.js +454 -0
  12. package/dist/agent/command.js.map +1 -0
  13. package/dist/agent/config.d.ts +30 -0
  14. package/dist/agent/config.d.ts.map +1 -0
  15. package/dist/agent/config.js +60 -0
  16. package/dist/agent/config.js.map +1 -0
  17. package/dist/agent/lock.d.ts +13 -0
  18. package/dist/agent/lock.d.ts.map +1 -0
  19. package/dist/agent/lock.js +100 -0
  20. package/dist/agent/lock.js.map +1 -0
  21. package/dist/agent/queue.d.ts +35 -0
  22. package/dist/agent/queue.d.ts.map +1 -0
  23. package/dist/agent/queue.js +50 -0
  24. package/dist/agent/queue.js.map +1 -0
  25. package/dist/agent/run.d.ts +97 -0
  26. package/dist/agent/run.d.ts.map +1 -0
  27. package/dist/agent/run.js +198 -0
  28. package/dist/agent/run.js.map +1 -0
  29. package/dist/agent/systemd.d.ts +55 -0
  30. package/dist/agent/systemd.d.ts.map +1 -0
  31. package/dist/agent/systemd.js +171 -0
  32. package/dist/agent/systemd.js.map +1 -0
  33. package/dist/agent/units.d.ts +44 -0
  34. package/dist/agent/units.d.ts.map +1 -0
  35. package/dist/agent/units.js +127 -0
  36. package/dist/agent/units.js.map +1 -0
  37. package/dist/agent-db.js +18362 -0
  38. package/dist/cli.js +119 -16
  39. package/dist/cli.js.map +1 -1
  40. package/dist/cloudsql-iam-user.d.ts +39 -0
  41. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  42. package/dist/cloudsql-iam-user.js +119 -0
  43. package/dist/cloudsql-iam-user.js.map +1 -0
  44. package/dist/compose.d.ts +16 -0
  45. package/dist/compose.d.ts.map +1 -1
  46. package/dist/compose.js +26 -1
  47. package/dist/compose.js.map +1 -1
  48. package/dist/dns/index.d.ts +2 -0
  49. package/dist/dns/index.d.ts.map +1 -1
  50. package/dist/dns/index.js +3 -2
  51. package/dist/dns/index.js.map +1 -1
  52. package/dist/dns/manual.d.ts +5 -0
  53. package/dist/dns/manual.d.ts.map +1 -1
  54. package/dist/dns/manual.js +28 -12
  55. package/dist/dns/manual.js.map +1 -1
  56. package/dist/gcloud.d.ts +2 -0
  57. package/dist/gcloud.d.ts.map +1 -1
  58. package/dist/gcloud.js +1 -1
  59. package/dist/gcloud.js.map +1 -1
  60. package/dist/gcp-names.d.ts +15 -0
  61. package/dist/gcp-names.d.ts.map +1 -1
  62. package/dist/gcp-names.js +23 -0
  63. package/dist/gcp-names.js.map +1 -1
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +4 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/infra-contract.d.ts +1 -1
  69. package/dist/infra-contract.js +7 -1
  70. package/dist/infra-contract.js.map +1 -1
  71. package/dist/install-job-runner.d.ts +13 -0
  72. package/dist/install-job-runner.d.ts.map +1 -0
  73. package/dist/install-job-runner.js +155 -0
  74. package/dist/install-job-runner.js.map +1 -0
  75. package/dist/install-job.d.ts +210 -0
  76. package/dist/install-job.d.ts.map +1 -0
  77. package/dist/install-job.js +449 -0
  78. package/dist/install-job.js.map +1 -0
  79. package/dist/install-state.d.ts +10 -1
  80. package/dist/install-state.d.ts.map +1 -1
  81. package/dist/install-state.js +10 -2
  82. package/dist/install-state.js.map +1 -1
  83. package/dist/lifecycle.d.ts.map +1 -1
  84. package/dist/lifecycle.js +19 -4
  85. package/dist/lifecycle.js.map +1 -1
  86. package/dist/migration-runner.d.ts +5 -0
  87. package/dist/migration-runner.d.ts.map +1 -1
  88. package/dist/migration-runner.js +1 -1
  89. package/dist/migration-runner.js.map +1 -1
  90. package/dist/operator-access.d.ts +14 -8
  91. package/dist/operator-access.d.ts.map +1 -1
  92. package/dist/operator-access.js +16 -102
  93. package/dist/operator-access.js.map +1 -1
  94. package/dist/run-mode.d.ts +27 -0
  95. package/dist/run-mode.d.ts.map +1 -0
  96. package/dist/run-mode.js +22 -0
  97. package/dist/run-mode.js.map +1 -0
  98. package/dist/runtime-secrets.d.ts +19 -0
  99. package/dist/runtime-secrets.d.ts.map +1 -1
  100. package/dist/runtime-secrets.js +44 -0
  101. package/dist/runtime-secrets.js.map +1 -1
  102. package/dist/self-hosted-roles.d.ts +22 -1
  103. package/dist/self-hosted-roles.d.ts.map +1 -1
  104. package/dist/self-hosted-roles.js +81 -12
  105. package/dist/self-hosted-roles.js.map +1 -1
  106. package/dist/self-hosted-upgrade.d.ts +17 -0
  107. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  108. package/dist/self-hosted-upgrade.js +16 -1
  109. package/dist/self-hosted-upgrade.js.map +1 -1
  110. package/dist/stack-contract.d.ts +6 -0
  111. package/dist/stack-contract.d.ts.map +1 -1
  112. package/dist/stack-contract.js +10 -1
  113. package/dist/stack-contract.js.map +1 -1
  114. package/dist/upgrade/converge.d.ts +6 -0
  115. package/dist/upgrade/converge.d.ts.map +1 -1
  116. package/dist/upgrade/converge.js +59 -2
  117. package/dist/upgrade/converge.js.map +1 -1
  118. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  119. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  120. package/dist/upgrade/gcp-lookup.js +25 -0
  121. package/dist/upgrade/gcp-lookup.js.map +1 -1
  122. package/dist/upgrade/release-images.d.ts +12 -0
  123. package/dist/upgrade/release-images.d.ts.map +1 -1
  124. package/dist/upgrade/release-images.js +31 -0
  125. package/dist/upgrade/release-images.js.map +1 -1
  126. package/dist/upgrade/upgrade.d.ts +5 -0
  127. package/dist/upgrade/upgrade.d.ts.map +1 -1
  128. package/dist/upgrade/upgrade.js +28 -1
  129. package/dist/upgrade/upgrade.js.map +1 -1
  130. package/dist/upgrader-assess.d.ts +82 -0
  131. package/dist/upgrader-assess.d.ts.map +1 -0
  132. package/dist/upgrader-assess.js +116 -0
  133. package/dist/upgrader-assess.js.map +1 -0
  134. package/dist/upgrader-gcp.d.ts +17 -0
  135. package/dist/upgrader-gcp.d.ts.map +1 -0
  136. package/dist/upgrader-gcp.js +87 -0
  137. package/dist/upgrader-gcp.js.map +1 -0
  138. package/dist/upgrader-job.d.ts +131 -0
  139. package/dist/upgrader-job.d.ts.map +1 -0
  140. package/dist/upgrader-job.js +259 -0
  141. package/dist/upgrader-job.js.map +1 -0
  142. package/dist/upgrader-reporter.d.ts +35 -0
  143. package/dist/upgrader-reporter.d.ts.map +1 -0
  144. package/dist/upgrader-reporter.js +79 -0
  145. package/dist/upgrader-reporter.js.map +1 -0
  146. package/dist/upgrader-setup.d.ts +59 -0
  147. package/dist/upgrader-setup.d.ts.map +1 -0
  148. package/dist/upgrader-setup.js +308 -0
  149. package/dist/upgrader-setup.js.map +1 -0
  150. package/dist/wizard.d.ts +6 -0
  151. package/dist/wizard.d.ts.map +1 -1
  152. package/dist/wizard.js +32 -4
  153. package/dist/wizard.js.map +1 -1
  154. package/dist/wizards/gcp-firebase.d.ts +3 -0
  155. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  156. package/dist/wizards/gcp-firebase.js +1 -0
  157. package/dist/wizards/gcp-firebase.js.map +1 -1
  158. package/dist/wizards/gcp-install-job.d.ts +74 -0
  159. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  160. package/dist/wizards/gcp-install-job.js +398 -0
  161. package/dist/wizards/gcp-install-job.js.map +1 -0
  162. package/dist/wizards/gcp-prompts.d.ts +3 -0
  163. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  164. package/dist/wizards/gcp-prompts.js +25 -0
  165. package/dist/wizards/gcp-prompts.js.map +1 -0
  166. package/dist/wizards/gcp-pulumi.d.ts +4 -2
  167. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  168. package/dist/wizards/gcp-pulumi.js +1 -1
  169. package/dist/wizards/gcp-pulumi.js.map +1 -1
  170. package/dist/wizards/gcp.d.ts +40 -0
  171. package/dist/wizards/gcp.d.ts.map +1 -1
  172. package/dist/wizards/gcp.js +223 -59
  173. package/dist/wizards/gcp.js.map +1 -1
  174. package/package.json +6 -4
  175. package/src/adapter.ts +97 -0
  176. package/src/adc-impersonation.ts +176 -0
  177. package/src/admin-domain.test.ts +254 -0
  178. package/src/admin-domain.ts +245 -0
  179. package/src/admin-memory.test.ts +151 -0
  180. package/src/agent/apply.test.ts +185 -0
  181. package/src/agent/apply.ts +124 -0
  182. package/src/agent/cli-provenance.test.ts +159 -0
  183. package/src/agent/cli-provenance.ts +191 -0
  184. package/src/agent/command.test.ts +77 -0
  185. package/src/agent/command.ts +564 -0
  186. package/src/agent/config.ts +86 -0
  187. package/src/agent/lock-config.test.ts +97 -0
  188. package/src/agent/lock.ts +110 -0
  189. package/src/agent/queue.test.ts +10 -0
  190. package/src/agent/queue.ts +91 -0
  191. package/src/agent/run.test.ts +382 -0
  192. package/src/agent/run.ts +295 -0
  193. package/src/agent/systemd.ts +209 -0
  194. package/src/agent/units.test.ts +223 -0
  195. package/src/agent/units.ts +163 -0
  196. package/src/azure-front-door.test.ts +119 -0
  197. package/src/azure-front-door.ts +56 -0
  198. package/src/bootstrap-token.ts +20 -0
  199. package/src/caddy.ts +112 -0
  200. package/src/cdn-copy.ts +91 -0
  201. package/src/cert-wait.test.ts +194 -0
  202. package/src/cert-wait.ts +163 -0
  203. package/src/checkpoint-secrets.test.ts +380 -0
  204. package/src/checkpoint-secrets.ts +283 -0
  205. package/src/cli-routing.test.ts +45 -0
  206. package/src/cli-routing.ts +43 -0
  207. package/src/cli.ts +1491 -0
  208. package/src/cloudsql-iam-user.ts +147 -0
  209. package/src/compose.ts +334 -0
  210. package/src/default-firewall.test.ts +411 -0
  211. package/src/default-firewall.ts +342 -0
  212. package/src/deployed-release.test.ts +231 -0
  213. package/src/deployed-release.ts +220 -0
  214. package/src/dns/cloudflare.ts +182 -0
  215. package/src/dns/groups.test.ts +76 -0
  216. package/src/dns/groups.ts +86 -0
  217. package/src/dns/index.ts +56 -0
  218. package/src/dns/manual.ts +130 -0
  219. package/src/dns/types.ts +27 -0
  220. package/src/gateway-credentials.test.ts +180 -0
  221. package/src/gateway-credentials.ts +144 -0
  222. package/src/gcloud-components.test.ts +76 -0
  223. package/src/gcloud-components.ts +85 -0
  224. package/src/gcloud-retry.ts +41 -0
  225. package/src/gcloud-runner.test.ts +61 -0
  226. package/src/gcloud.test.ts +99 -0
  227. package/src/gcloud.ts +322 -0
  228. package/src/gcp-names.ts +135 -0
  229. package/src/index.test.ts +332 -0
  230. package/src/index.ts +114 -0
  231. package/src/infra-contract.ts +32 -0
  232. package/src/install-job-runner.ts +166 -0
  233. package/src/install-job.test.ts +566 -0
  234. package/src/install-job.ts +542 -0
  235. package/src/install-state.test.ts +239 -0
  236. package/src/install-state.ts +583 -0
  237. package/src/internal-token.ts +35 -0
  238. package/src/lifecycle.ts +900 -0
  239. package/src/mcp-iap-stacks.test.ts +58 -0
  240. package/src/mcp-iap.test.ts +149 -0
  241. package/src/mcp-iap.ts +131 -0
  242. package/src/media-volume.test.ts +296 -0
  243. package/src/migration-runner.ts +365 -0
  244. package/src/operator-access-stacks.test.ts +55 -0
  245. package/src/operator-access.test.ts +332 -0
  246. package/src/operator-access.ts +554 -0
  247. package/src/ops-backend.test.ts +320 -0
  248. package/src/ops-backend.ts +414 -0
  249. package/src/provisioner-identity.test.ts +482 -0
  250. package/src/provisioner-identity.ts +407 -0
  251. package/src/redirects-emit.ts +175 -0
  252. package/src/region-data.ts +428 -0
  253. package/src/regions.test.ts +277 -0
  254. package/src/regions.ts +370 -0
  255. package/src/release-contract.ts +57 -0
  256. package/src/release-manifest.test.ts +293 -0
  257. package/src/release-manifest.ts +430 -0
  258. package/src/run-mode.ts +40 -0
  259. package/src/runtime-secrets.test.ts +627 -0
  260. package/src/runtime-secrets.ts +754 -0
  261. package/src/self-command.test.ts +14 -0
  262. package/src/self-command.ts +21 -0
  263. package/src/self-hosted-compose.test.ts +177 -0
  264. package/src/self-hosted-release.ts +240 -0
  265. package/src/self-hosted-roles.integration.test.ts +161 -0
  266. package/src/self-hosted-roles.test.ts +124 -0
  267. package/src/self-hosted-roles.ts +203 -0
  268. package/src/self-hosted-upgrade.test.ts +394 -0
  269. package/src/self-hosted-upgrade.ts +548 -0
  270. package/src/stack-contract-parity.test.ts +199 -0
  271. package/src/stack-contract.test.ts +391 -0
  272. package/src/stack-contract.ts +888 -0
  273. package/src/stack-converge-secrets.test.ts +421 -0
  274. package/src/stack-converge.test.ts +833 -0
  275. package/src/stack-converge.ts +909 -0
  276. package/src/staging-admin.test.ts +246 -0
  277. package/src/staging-admin.ts +324 -0
  278. package/src/state-attach.test.ts +211 -0
  279. package/src/state-attach.ts +132 -0
  280. package/src/state-backend.test.ts +145 -0
  281. package/src/state-backend.ts +262 -0
  282. package/src/state-command.ts +293 -0
  283. package/src/state-migrate.test.ts +479 -0
  284. package/src/state-migrate.ts +434 -0
  285. package/src/state-pulumi.ts +289 -0
  286. package/src/state-store.test.ts +212 -0
  287. package/src/state-store.ts +243 -0
  288. package/src/upgrade/app-roll.ts +486 -0
  289. package/src/upgrade/backup.ts +109 -0
  290. package/src/upgrade/converge.ts +437 -0
  291. package/src/upgrade/gcp-lookup.ts +167 -0
  292. package/src/upgrade/health.test.ts +279 -0
  293. package/src/upgrade/health.ts +305 -0
  294. package/src/upgrade/release-images.test.ts +119 -0
  295. package/src/upgrade/release-images.ts +391 -0
  296. package/src/upgrade/reporter.ts +46 -0
  297. package/src/upgrade/upgrade.test.ts +626 -0
  298. package/src/upgrade/upgrade.ts +249 -0
  299. package/src/upgrader-assess.test.ts +265 -0
  300. package/src/upgrader-assess.ts +233 -0
  301. package/src/upgrader-gcp.ts +123 -0
  302. package/src/upgrader-job.test.ts +408 -0
  303. package/src/upgrader-job.ts +499 -0
  304. package/src/upgrader-reporter.test.ts +46 -0
  305. package/src/upgrader-reporter.ts +105 -0
  306. package/src/upgrader-setup.test.ts +169 -0
  307. package/src/upgrader-setup.ts +355 -0
  308. package/src/wizard.ts +425 -0
  309. package/src/wizards/gcp-cost.ts +188 -0
  310. package/src/wizards/gcp-firebase.ts +72 -0
  311. package/src/wizards/gcp-install-job.ts +506 -0
  312. package/src/wizards/gcp-prompts.ts +26 -0
  313. package/src/wizards/gcp-pulumi.test.ts +246 -0
  314. package/src/wizards/gcp-pulumi.ts +516 -0
  315. 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
+ }