@omega.js/manager 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.
Files changed (416) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/LICENSE +98 -0
  3. package/README.md +165 -0
  4. package/bin/omega +2 -0
  5. package/bin/omg +2 -0
  6. package/claude-plugin/.claude-plugin/plugin.json +11 -0
  7. package/claude-plugin/.mcp.json +9 -0
  8. package/claude-plugin/README.md +139 -0
  9. package/claude-plugin/hooks/gate/mark.sh +58 -0
  10. package/claude-plugin/hooks/gate/run.sh +97 -0
  11. package/claude-plugin/hooks/guard/run.sh +249 -0
  12. package/claude-plugin/hooks/hooks.json +63 -0
  13. package/claude-plugin/hooks/inject/run.sh +127 -0
  14. package/claude-plugin/hooks/lib/omega-gate.sh +20 -0
  15. package/claude-plugin/hooks/lib/omega-scope.sh +41 -0
  16. package/claude-plugin/hooks/lib/omega-skills.sh +209 -0
  17. package/claude-plugin/hooks/quality/run.sh +132 -0
  18. package/claude-plugin/hooks/shape/run.sh +45 -0
  19. package/claude-plugin/mcp-router-launch.js +29 -0
  20. package/claude-plugin/skills/README.md +48 -0
  21. package/claude-plugin/skills/accessibility/SKILL.md +31 -0
  22. package/claude-plugin/skills/analytics/SKILL.md +30 -0
  23. package/claude-plugin/skills/backend/SKILL.md +25 -0
  24. package/claude-plugin/skills/brandcheck/SKILL.md +29 -0
  25. package/claude-plugin/skills/browser/SKILL.md +64 -0
  26. package/claude-plugin/skills/client/SKILL.md +24 -0
  27. package/claude-plugin/skills/desktop/SKILL.md +25 -0
  28. package/claude-plugin/skills/extension/SKILL.md +25 -0
  29. package/claude-plugin/skills/main/SKILL.md +54 -0
  30. package/claude-plugin/skills/manager/SKILL.md +25 -0
  31. package/claude-plugin/skills/seo/SKILL.md +36 -0
  32. package/claude-plugin/skills/theme/SKILL.md +33 -0
  33. package/claude-plugin/skills/web/SKILL.md +29 -0
  34. package/dist/cli-run.js +34 -0
  35. package/dist/cli.js +29 -0
  36. package/dist/commands/build.js +17 -0
  37. package/dist/commands/clean.js +16 -0
  38. package/dist/commands/company.js +46 -0
  39. package/dist/commands/deploy.js +143 -0
  40. package/dist/commands/dev.js +437 -0
  41. package/dist/commands/devlog.js +25 -0
  42. package/dist/commands/help.js +48 -0
  43. package/dist/commands/manage.js +53 -0
  44. package/dist/commands/migrate.js +76 -0
  45. package/dist/commands/onboard.js +31 -0
  46. package/dist/commands/pipeline.js +319 -0
  47. package/dist/commands/test.js +380 -0
  48. package/dist/commands/update.js +128 -0
  49. package/dist/commands/version.js +8 -0
  50. package/dist/company-init.js +167 -0
  51. package/dist/company.js +283 -0
  52. package/dist/config.js +1359 -0
  53. package/dist/devlog/index.js +226 -0
  54. package/dist/devlog/lib/collect-commits.js +136 -0
  55. package/dist/devlog/lib/generate-post.js +201 -0
  56. package/dist/devlog/lib/ghostii.js +122 -0
  57. package/dist/devlog/lib/project-map.js +98 -0
  58. package/dist/devlog/lib/publish-website.js +88 -0
  59. package/dist/index.js +45 -0
  60. package/dist/lib/agents-md.js +201 -0
  61. package/dist/lib/analytics-secret.js +22 -0
  62. package/dist/lib/argv.js +21 -0
  63. package/dist/lib/auth-admin.js +120 -0
  64. package/dist/lib/automation-client.js +210 -0
  65. package/dist/lib/backend-marketing.js +49 -0
  66. package/dist/lib/brand.js +240 -0
  67. package/dist/lib/bundle-id.js +53 -0
  68. package/dist/lib/claude-settings.js +125 -0
  69. package/dist/lib/company-scaffold.js +213 -0
  70. package/dist/lib/company.js +227 -0
  71. package/dist/lib/config-flow.js +350 -0
  72. package/dist/lib/config-write.js +41 -0
  73. package/dist/lib/custom-target.js +75 -0
  74. package/dist/lib/domain-utils.js +19 -0
  75. package/dist/lib/duration.js +39 -0
  76. package/dist/lib/env-order.js +236 -0
  77. package/dist/lib/env-secret.js +78 -0
  78. package/dist/lib/firestore-rest.js +326 -0
  79. package/dist/lib/framework-bin.js +156 -0
  80. package/dist/lib/gitignore.js +63 -0
  81. package/dist/lib/google-auth.js +495 -0
  82. package/dist/lib/google-token.js +66 -0
  83. package/dist/lib/jwt.js +31 -0
  84. package/dist/lib/legacy-oauth.js +59 -0
  85. package/dist/lib/node-version.js +127 -0
  86. package/dist/lib/owner-plan.js +78 -0
  87. package/dist/lib/package-scripts.js +152 -0
  88. package/dist/lib/preflight.js +470 -0
  89. package/dist/lib/product-create.js +144 -0
  90. package/dist/lib/run-command.js +74 -0
  91. package/dist/lib/run-gates.js +70 -0
  92. package/dist/lib/run-output.js +42 -0
  93. package/dist/lib/run-summary.js +384 -0
  94. package/dist/lib/scaffold.js +427 -0
  95. package/dist/lib/service-input.js +207 -0
  96. package/dist/lib/service-runner.js +466 -0
  97. package/dist/lib/stale.js +24 -0
  98. package/dist/lib/target-selection.js +155 -0
  99. package/dist/lib/verb-fanout.js +128 -0
  100. package/dist/lib/verify-live.js +188 -0
  101. package/dist/manage.js +311 -0
  102. package/dist/omega-bin.js +7 -0
  103. package/dist/onboard.js +532 -0
  104. package/dist/services/account/ensure/users.js +259 -0
  105. package/dist/services/account/index.js +97 -0
  106. package/dist/services/account/lib/backend-client.js +107 -0
  107. package/dist/services/account/lib/password.js +36 -0
  108. package/dist/services/account/lib/resolve-password.js +100 -0
  109. package/dist/services/advertising/ensure/sites.js +101 -0
  110. package/dist/services/advertising/index.js +101 -0
  111. package/dist/services/advertising/lib/adsense-api.js +41 -0
  112. package/dist/services/ai/ensure/keys.js +21 -0
  113. package/dist/services/ai/index.js +36 -0
  114. package/dist/services/analytics/ensure/google-firebase-link.js +230 -0
  115. package/dist/services/analytics/ensure/google-streams.js +256 -0
  116. package/dist/services/analytics/ensure/meta-pixel.js +22 -0
  117. package/dist/services/analytics/ensure/tiktok-pixel.js +22 -0
  118. package/dist/services/analytics/index.js +134 -0
  119. package/dist/services/analytics/lib/analytics-api.js +156 -0
  120. package/dist/services/analytics/lib/meta-api.js +83 -0
  121. package/dist/services/analytics/lib/pixel-account.js +78 -0
  122. package/dist/services/analytics/lib/pixel-provision.js +152 -0
  123. package/dist/services/analytics/lib/pixel-specs.js +57 -0
  124. package/dist/services/analytics/lib/pixel-token.js +88 -0
  125. package/dist/services/analytics/lib/property-flow.js +77 -0
  126. package/dist/services/analytics/lib/tiktok-api.js +157 -0
  127. package/dist/services/analytics/lib/tiktok-auth.js +153 -0
  128. package/dist/services/assets/ensure/logo-gen.js +68 -0
  129. package/dist/services/assets/index.js +109 -0
  130. package/dist/services/assets/lib/assets-config.js +168 -0
  131. package/dist/services/assets/lib/brandmark-api.js +160 -0
  132. package/dist/services/assets/lib/derived.js +113 -0
  133. package/dist/services/assets/lib/font-loader.js +67 -0
  134. package/dist/services/assets/lib/reconcile.js +122 -0
  135. package/dist/services/assets/lib/reset.js +118 -0
  136. package/dist/services/assets/lib/svg-logo-generator.js +154 -0
  137. package/dist/services/assets/lib/svg-to-black.js +32 -0
  138. package/dist/services/assets/write/favicons.js +82 -0
  139. package/dist/services/assets/write/icons.js +64 -0
  140. package/dist/services/assets/write/process.js +96 -0
  141. package/dist/services/assets/write/reconcile.js +30 -0
  142. package/dist/services/assets/write/social-icons.js +108 -0
  143. package/dist/services/assets/write/templates.js +345 -0
  144. package/dist/services/bookmark/ensure/sync.js +255 -0
  145. package/dist/services/bookmark/index.js +17 -0
  146. package/dist/services/campaigns/ensure/contact-person.js +40 -0
  147. package/dist/services/campaigns/ensure/custom-fields.js +84 -0
  148. package/dist/services/campaigns/ensure/domain-auth.js +92 -0
  149. package/dist/services/campaigns/ensure/event-webhook.js +89 -0
  150. package/dist/services/campaigns/ensure/link-branding.js +187 -0
  151. package/dist/services/campaigns/ensure/list.js +50 -0
  152. package/dist/services/campaigns/ensure/segments.js +109 -0
  153. package/dist/services/campaigns/ensure/sender-identity.js +153 -0
  154. package/dist/services/campaigns/ensure/unsubscribe-groups.js +116 -0
  155. package/dist/services/campaigns/index.js +79 -0
  156. package/dist/services/campaigns/lib/dns-sync.js +81 -0
  157. package/dist/services/campaigns/lib/segment-query.js +138 -0
  158. package/dist/services/campaigns/lib/sendgrid-api.js +287 -0
  159. package/dist/services/captcha/ensure/site-key.js +83 -0
  160. package/dist/services/captcha/index.js +78 -0
  161. package/dist/services/captcha/lib/console-url.js +45 -0
  162. package/dist/services/captcha/lib/recaptcha-api.js +36 -0
  163. package/dist/services/certificates/ensure/api-key.js +22 -0
  164. package/dist/services/certificates/ensure/bundle-ids.js +115 -0
  165. package/dist/services/certificates/ensure/certificates.js +187 -0
  166. package/dist/services/certificates/ensure/profiles.js +143 -0
  167. package/dist/services/certificates/index.js +225 -0
  168. package/dist/services/certificates/lib/apple-api.js +183 -0
  169. package/dist/services/certificates/lib/certificate-manager.js +252 -0
  170. package/dist/services/certificates/lib/identifier-manager.js +107 -0
  171. package/dist/services/certificates/lib/keychain.js +135 -0
  172. package/dist/services/certificates/lib/manual-walkthrough.js +165 -0
  173. package/dist/services/certificates/lib/profile-manager.js +115 -0
  174. package/dist/services/chat/data/baseline-knowledge.md +96 -0
  175. package/dist/services/chat/ensure/chat.js +106 -0
  176. package/dist/services/chat/ensure/user.js +45 -0
  177. package/dist/services/chat/index.js +139 -0
  178. package/dist/services/chat/lib/baseline-knowledge.js +137 -0
  179. package/dist/services/cloud/ensure/authentication.js +267 -0
  180. package/dist/services/cloud/ensure/billing.js +120 -0
  181. package/dist/services/cloud/ensure/cloud-messaging.js +105 -0
  182. package/dist/services/cloud/ensure/database.js +60 -0
  183. package/dist/services/cloud/ensure/firestore.js +79 -0
  184. package/dist/services/cloud/ensure/functions.js +49 -0
  185. package/dist/services/cloud/ensure/hosting.js +398 -0
  186. package/dist/services/cloud/ensure/oauth-consent.js +248 -0
  187. package/dist/services/cloud/ensure/project-settings.js +72 -0
  188. package/dist/services/cloud/ensure/sdk-config.js +95 -0
  189. package/dist/services/cloud/ensure/service-account.js +152 -0
  190. package/dist/services/cloud/ensure/services.js +103 -0
  191. package/dist/services/cloud/ensure/storage.js +47 -0
  192. package/dist/services/cloud/index.js +130 -0
  193. package/dist/services/cloud/lib/access-heal.js +130 -0
  194. package/dist/services/cloud/lib/firebase-api.js +662 -0
  195. package/dist/services/cloud/lib/project-flow.js +102 -0
  196. package/dist/services/directory/ensure/entry.js +48 -0
  197. package/dist/services/directory/index.js +64 -0
  198. package/dist/services/directory/lib/blocks.js +104 -0
  199. package/dist/services/disperse/index.js +45 -0
  200. package/dist/services/disperse/write/certs.js +117 -0
  201. package/dist/services/domain/ensure/nameservers.js +179 -0
  202. package/dist/services/domain/index.js +69 -0
  203. package/dist/services/domain/lib/namecheap-api.js +167 -0
  204. package/dist/services/domain/lib/registrars.js +46 -0
  205. package/dist/services/domain/lib/whitelist-walkthrough.js +76 -0
  206. package/dist/services/edge/ensure/cache-rules.js +105 -0
  207. package/dist/services/edge/ensure/dns-records.js +304 -0
  208. package/dist/services/edge/ensure/email-routing.js +292 -0
  209. package/dist/services/edge/ensure/rules-configuration.js +98 -0
  210. package/dist/services/edge/ensure/rules-managed-transforms.js +101 -0
  211. package/dist/services/edge/ensure/rules-redirect.js +107 -0
  212. package/dist/services/edge/ensure/rules-response-headers.js +90 -0
  213. package/dist/services/edge/ensure/rules-security.js +142 -0
  214. package/dist/services/edge/ensure/speed-scheduled-tests.js +98 -0
  215. package/dist/services/edge/ensure/workers.js +219 -0
  216. package/dist/services/edge/ensure/zone-settings.js +108 -0
  217. package/dist/services/edge/ensure/zone.js +183 -0
  218. package/dist/services/edge/index.js +73 -0
  219. package/dist/services/edge/lib/cloudflare-api.js +84 -0
  220. package/dist/services/edge/lib/dns-records-helpers.js +518 -0
  221. package/dist/services/edge/lib/read-cache.js +16 -0
  222. package/dist/services/edge/lib/ruleset-helper.js +108 -0
  223. package/dist/services/edge/workers/omega-api-proxy.js +43 -0
  224. package/dist/services/email/data/baseline-filter.md +4 -0
  225. package/dist/services/email/data/baseline-knowledge.md +86 -0
  226. package/dist/services/email/ensure/agent.js +107 -0
  227. package/dist/services/email/ensure/user.js +45 -0
  228. package/dist/services/email/index.js +138 -0
  229. package/dist/services/email/lib/baseline.js +82 -0
  230. package/dist/services/email/lib/knowledge-file.js +53 -0
  231. package/dist/services/forms/ensure/form.js +46 -0
  232. package/dist/services/forms/ensure/user.js +45 -0
  233. package/dist/services/forms/index.js +138 -0
  234. package/dist/services/migrations/ensure/notifications.js +189 -0
  235. package/dist/services/migrations/ensure/orders.js +34 -0
  236. package/dist/services/migrations/ensure/payment-provider.js +132 -0
  237. package/dist/services/migrations/ensure/payments-intents.js +34 -0
  238. package/dist/services/migrations/ensure/state-retirement.js +266 -0
  239. package/dist/services/migrations/ensure/targets-rename.js +124 -0
  240. package/dist/services/migrations/ensure/users.js +1176 -0
  241. package/dist/services/migrations/index.js +101 -0
  242. package/dist/services/migrations/lib/attribution-touch.js +64 -0
  243. package/dist/services/migrations/lib/ensure-metadata.js +144 -0
  244. package/dist/services/migrations/lib/migration-runner.js +473 -0
  245. package/dist/services/migrations/lib/sanitize-strings.js +112 -0
  246. package/dist/services/migrations/lib/schema-validator.js +154 -0
  247. package/dist/services/monitoring/ensure/dsn.js +54 -0
  248. package/dist/services/monitoring/ensure/projects.js +135 -0
  249. package/dist/services/monitoring/index.js +54 -0
  250. package/dist/services/monitoring/lib/sentry-api.js +94 -0
  251. package/dist/services/newsletter/ensure/custom-fields.js +90 -0
  252. package/dist/services/newsletter/ensure/publication.js +94 -0
  253. package/dist/services/newsletter/ensure/segments.js +140 -0
  254. package/dist/services/newsletter/ensure/webhook.js +108 -0
  255. package/dist/services/newsletter/index.js +64 -0
  256. package/dist/services/newsletter/lib/beehiiv-api.js +124 -0
  257. package/dist/services/newsletter/lib/segment-automation.js +577 -0
  258. package/dist/services/payment/ensure/chargebee-account.js +30 -0
  259. package/dist/services/payment/ensure/chargebee-products.js +359 -0
  260. package/dist/services/payment/ensure/chargebee-webhook.js +113 -0
  261. package/dist/services/payment/ensure/paypal-account.js +36 -0
  262. package/dist/services/payment/ensure/paypal-products.js +374 -0
  263. package/dist/services/payment/ensure/paypal-webhook.js +128 -0
  264. package/dist/services/payment/ensure/stripe-account.js +93 -0
  265. package/dist/services/payment/ensure/stripe-disputes.js +56 -0
  266. package/dist/services/payment/ensure/stripe-products.js +245 -0
  267. package/dist/services/payment/ensure/stripe-radar.js +74 -0
  268. package/dist/services/payment/ensure/stripe-webhook.js +130 -0
  269. package/dist/services/payment/index.js +163 -0
  270. package/dist/services/payment/lib/chargebee-api.js +338 -0
  271. package/dist/services/payment/lib/payment-utils.js +116 -0
  272. package/dist/services/payment/lib/paypal-api.js +354 -0
  273. package/dist/services/payment/lib/provider-setup.js +134 -0
  274. package/dist/services/payment/lib/stripe-api.js +152 -0
  275. package/dist/services/repo/ensure/org.js +70 -0
  276. package/dist/services/repo/ensure/pages.js +72 -0
  277. package/dist/services/repo/ensure/repo.js +90 -0
  278. package/dist/services/repo/index.js +52 -0
  279. package/dist/services/repo/lib/github-api.js +196 -0
  280. package/dist/services/search/ensure/ga-link.js +57 -0
  281. package/dist/services/search/ensure/property.js +137 -0
  282. package/dist/services/search/ensure/sitemaps.js +70 -0
  283. package/dist/services/search/index.js +61 -0
  284. package/dist/services/search/lib/search-console-api.js +92 -0
  285. package/dist/services/seo/ensure/github-repos.js +271 -0
  286. package/dist/services/seo/index.js +55 -0
  287. package/dist/services/seo/lib/gh-api.js +161 -0
  288. package/dist/services/seo/templates/developer-tool/.github/workflows/maintenance.yml +25 -0
  289. package/dist/services/seo/templates/developer-tool/.nvmrc +1 -0
  290. package/dist/services/seo/templates/developer-tool/_README.md.js +121 -0
  291. package/dist/services/seo/templates/developer-tool/_package.json.js +33 -0
  292. package/dist/services/seo/templates/developer-tool/src/index.js +120 -0
  293. package/dist/services/seo/templates/index.js +57 -0
  294. package/dist/services/server/ensure/brands.js +63 -0
  295. package/dist/services/server/index.js +42 -0
  296. package/dist/services/testing/ensure/target-checks.js +97 -0
  297. package/dist/services/testing/index.js +11 -0
  298. package/dist/services/testing/lib/checks.js +467 -0
  299. package/dist/services/update/index.js +20 -0
  300. package/dist/services/update/lib/cache.js +59 -0
  301. package/dist/services/update/lib/fingerprint.js +157 -0
  302. package/dist/services/update/write/targets.js +206 -0
  303. package/dist/services/workspace/ensure/agents.js +42 -0
  304. package/dist/services/workspace/ensure/claude-settings.js +34 -0
  305. package/dist/services/workspace/ensure/config.js +54 -0
  306. package/dist/services/workspace/ensure/defaults.js +60 -0
  307. package/dist/services/workspace/ensure/env-keys.js +55 -0
  308. package/dist/services/workspace/ensure/env-order.js +66 -0
  309. package/dist/services/workspace/ensure/env-rules.js +97 -0
  310. package/dist/services/workspace/ensure/gitignore.js +18 -0
  311. package/dist/services/workspace/ensure/scripts.js +126 -0
  312. package/dist/services/workspace/ensure/structure.js +97 -0
  313. package/dist/services/workspace/ensure/translation-sdk.js +148 -0
  314. package/dist/services/workspace/ensure/workflows.js +54 -0
  315. package/dist/services/workspace/index.js +8 -0
  316. package/dist/vendor/account/engine.js +182 -0
  317. package/dist/vendor/account/features.js +220 -0
  318. package/dist/vendor/account/index.js +53 -0
  319. package/dist/vendor/account/schema.js +272 -0
  320. package/dist/vendor/account/subscription.js +38 -0
  321. package/dist/vendor/config/company.js +31 -0
  322. package/dist/vendor/config/defaults.js +173 -0
  323. package/dist/vendor/config/demo.js +18 -0
  324. package/dist/vendor/config/desktop-artifacts.js +110 -0
  325. package/dist/vendor/config/edit.js +769 -0
  326. package/dist/vendor/config/env-delivery.js +145 -0
  327. package/dist/vendor/config/env-rules.js +93 -0
  328. package/dist/vendor/config/env-schema.js +1078 -0
  329. package/dist/vendor/config/env.js +445 -0
  330. package/dist/vendor/config/hooks.js +97 -0
  331. package/dist/vendor/config/index.js +237 -0
  332. package/dist/vendor/config/instances.js +208 -0
  333. package/dist/vendor/config/load.js +490 -0
  334. package/dist/vendor/config/merge.js +68 -0
  335. package/dist/vendor/config/order.js +139 -0
  336. package/dist/vendor/config/ports.js +374 -0
  337. package/dist/vendor/config/providers.js +32 -0
  338. package/dist/vendor/config/repo.js +142 -0
  339. package/dist/vendor/config/retired-keys.js +430 -0
  340. package/dist/vendor/config/schema.js +1610 -0
  341. package/dist/vendor/config/secrets.js +50 -0
  342. package/dist/vendor/config/seed.js +34 -0
  343. package/dist/vendor/config/site-global.js +205 -0
  344. package/dist/vendor/config/validate.js +554 -0
  345. package/dist/vendor/config/winback.js +61 -0
  346. package/dist/vendor/devkit/attach-log-file.js +262 -0
  347. package/dist/vendor/devkit/certs.js +199 -0
  348. package/dist/vendor/devkit/ci-workflows.js +520 -0
  349. package/dist/vendor/devkit/cli-router.js +156 -0
  350. package/dist/vendor/devkit/command-path.js +46 -0
  351. package/dist/vendor/devkit/deploy-record.js +180 -0
  352. package/dist/vendor/devkit/flows.js +317 -0
  353. package/dist/vendor/devkit/local-https.js +360 -0
  354. package/dist/vendor/devkit/local.js +1905 -0
  355. package/dist/vendor/devkit/logger.js +128 -0
  356. package/dist/vendor/devkit/omega-bin.js +345 -0
  357. package/dist/vendor/devkit/prompt.js +187 -0
  358. package/dist/vendor/devkit/safe-install.js +18 -0
  359. package/dist/vendor/devkit/stop-signals.js +28 -0
  360. package/dist/vendor/devkit/test/scope.js +162 -0
  361. package/dist/vendor/devkit/translate/cache.js +84 -0
  362. package/dist/vendor/devkit/translate/engine.js +243 -0
  363. package/dist/vendor/devkit/translate/index.js +49 -0
  364. package/dist/vendor/devkit/translate/languages.js +134 -0
  365. package/dist/vendor/devkit/translate/providers.js +188 -0
  366. package/dist/vendor/devkit/update.js +569 -0
  367. package/docs/AGENTS.md +200 -0
  368. package/docs/account.md +45 -0
  369. package/docs/advertising.md +46 -0
  370. package/docs/ai.md +34 -0
  371. package/docs/analytics.md +69 -0
  372. package/docs/assets.md +57 -0
  373. package/docs/bookmark.md +26 -0
  374. package/docs/brand.md +151 -0
  375. package/docs/campaigns.md +96 -0
  376. package/docs/captcha.md +46 -0
  377. package/docs/certificates.md +68 -0
  378. package/docs/chat.md +47 -0
  379. package/docs/cloud.md +110 -0
  380. package/docs/company.md +64 -0
  381. package/docs/directory.md +140 -0
  382. package/docs/disperse.md +46 -0
  383. package/docs/domain.md +56 -0
  384. package/docs/edge.md +234 -0
  385. package/docs/email.md +42 -0
  386. package/docs/forms.md +48 -0
  387. package/docs/index.md +109 -0
  388. package/docs/migration.md +203 -0
  389. package/docs/migrations.md +56 -0
  390. package/docs/monitoring.md +41 -0
  391. package/docs/newsletter.md +48 -0
  392. package/docs/payment.md +83 -0
  393. package/docs/repo.md +48 -0
  394. package/docs/search.md +44 -0
  395. package/docs/seo.md +34 -0
  396. package/docs/server.md +37 -0
  397. package/docs/shared/agent-docs.md +89 -0
  398. package/docs/shared/analytics.md +612 -0
  399. package/docs/shared/brands.md +51 -0
  400. package/docs/shared/breaking-changes.md +497 -0
  401. package/docs/shared/config.md +1387 -0
  402. package/docs/shared/deploys.md +215 -0
  403. package/docs/shared/icons.md +201 -0
  404. package/docs/shared/local-dev.md +147 -0
  405. package/docs/shared/logging.md +202 -0
  406. package/docs/shared/monitoring.md +153 -0
  407. package/docs/shared/publishing.md +183 -0
  408. package/docs/shared/rulings.md +34 -0
  409. package/docs/shared/testing.md +147 -0
  410. package/docs/shared/theming.md +604 -0
  411. package/docs/shared/translation.md +291 -0
  412. package/docs/shared/updates.md +61 -0
  413. package/docs/testing.md +34 -0
  414. package/docs/update.md +36 -0
  415. package/docs/workspace.md +64 -0
  416. package/package.json +88 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Ensure the brand's domain is present in AdSense and report its approval
3
+ * state.
4
+ *
5
+ * The Management API v2 has no writes, so presence with a READY state is the
6
+ * converged proof and everything else is guidance: missing → interactive runs
7
+ * open the add-site console page and poll until the site appears,
8
+ * non-interactive/dry runs warn with the deep-link (rerun converges once it
9
+ * appears); non-READY → warned with what Google is waiting on.
10
+ */
11
+ const chalk = require('chalk').default;
12
+ const { openBrowserAndPoll } = require('../../../vendor/devkit/flows.js');
13
+ const { canPrompt } = require('../../../lib/run-gates.js');
14
+
15
+ // AdSense site approval states → run status + operator guidance
16
+ const STATES = {
17
+ READY: { status: 'success', label: 'Ready (serving ads)' },
18
+ GETTING_READY: { status: 'warned', label: 'Getting ready — Google is reviewing the site; rerun once approved' },
19
+ REQUIRES_REVIEW: { status: 'warned', label: 'Requires review — request one in the AdSense console' },
20
+ NEEDS_ATTENTION: { status: 'warned', label: 'Needs attention — resolve the issues in the AdSense console' },
21
+ };
22
+
23
+ module.exports = async function ensureSites(context) {
24
+ const { adsenseApi, accountId, domain, options = {} } = context;
25
+
26
+ const sitesUrl = `https://adsense.google.com/adsense/u/0/${accountId}/sites/list?url=${domain}`;
27
+ // AdSense lists sites at the apex only, so an apex entry covers every
28
+ // subdomain of it (#631)
29
+ const matchesDomain = (entry) => entry.domain === domain || entry.domain === `www.${domain}` || domain.endsWith(`.${entry.domain}`);
30
+
31
+ const sites = await adsenseApi.listSites(accountId);
32
+ const site = sites.find(matchesDomain);
33
+
34
+ if (site) {
35
+ return reportSite(site, sitesUrl, domain);
36
+ }
37
+
38
+ console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(domain)} is not added to AdSense ${chalk.dim('(no API exists to add it)')}`);
39
+
40
+ // Interactive runs open the add-site page and poll until it appears
41
+ if (canPrompt(options)) {
42
+ const result = await openBrowserAndPoll({
43
+ url: sitesUrl,
44
+ label: 'the AdSense sites page',
45
+ promptMessage: `Add ${chalk.cyan(domain)} as a site in the AdSense console.`,
46
+ waitMessage: 'Waiting for the site to appear',
47
+ check: async () => {
48
+ const fresh = (await adsenseApi.listSites(accountId)).find(matchesDomain);
49
+ return fresh ? { done: true, result: fresh } : { done: false };
50
+ },
51
+ intervalMs: 5000,
52
+ indent: ' ',
53
+ });
54
+
55
+ if (result.success && result.result) {
56
+ return reportSite(result.result, sitesUrl, domain);
57
+ }
58
+ }
59
+
60
+ console.log(` ${chalk.dim('→')} Add the site at: ${chalk.cyan(sitesUrl)}`);
61
+ console.log(` ${chalk.dim('→')} (rerun converges once it appears)`);
62
+ return { status: 'warned', reason: 'site not added yet — add it in the AdSense console', output: { sites: { domain, state: null, addUrl: sitesUrl } } };
63
+ };
64
+
65
+ /**
66
+ * Report a present site's approval state (READY = success, anything else =
67
+ * warned with the console deep-link). A parent-domain match reports the
68
+ * apex's state and says so (#631).
69
+ */
70
+ function reportSite(site, sitesUrl, domain) {
71
+ const state = site.state || 'STATE_UNSPECIFIED';
72
+ const display = STATES[state] || { status: 'warned', label: state };
73
+ const covered = site.domain !== domain && domain.endsWith(`.${site.domain}`);
74
+ const subject = covered
75
+ ? `${chalk.cyan(domain)} is covered by ${chalk.cyan(site.domain)}`
76
+ : chalk.cyan(site.domain);
77
+
78
+ if (display.status === 'success') {
79
+ console.log(` ${chalk.green('✓')} ${subject}: ${display.label}`);
80
+ } else {
81
+ console.log(` ${chalk.yellow('⚠')} ${subject}: ${display.label}`);
82
+ console.log(` ${chalk.dim('→')} ${chalk.cyan(sitesUrl)}`);
83
+ }
84
+
85
+ if (site.autoAdsEnabled !== undefined) {
86
+ console.log(` ${chalk.dim('→')} Auto ads: ${site.autoAdsEnabled ? 'enabled' : 'disabled'}`);
87
+ }
88
+
89
+ return {
90
+ status: display.status,
91
+ ...(display.status === 'warned' ? { reason: display.label } : {}),
92
+ output: {
93
+ sites: {
94
+ domain: site.domain,
95
+ state,
96
+ autoAdsEnabled: site.autoAdsEnabled,
97
+ ...(covered ? { coveredBy: site.domain } : {}),
98
+ },
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Advertising service — verifies the brand's domain is added to the
3
+ * configured AdSense account and reports its approval state.
4
+ *
5
+ * The Management API v2 is read-only (sites can't be added or configured
6
+ * programmatically), so this service proves presence + state and deep-links
7
+ * the console for the manual half; interactive runs open the add-site page
8
+ * and poll until the site appears. Never mutates AdSense — dry-run is
9
+ * identical to a normal run.
10
+ *
11
+ * Config home: `advertising.providers.adsense` (the provider-neutral
12
+ * advertising section — never a brand-named top-level key). `client`
13
+ * (ca-pub-…) is required config; the API accountId is the same id without
14
+ * the `ca-` prefix. omega-manager defaulted the account to the company's
15
+ * shared one (company-level config supplies that now). Interactive runs
16
+ * offer the account selection flow when it's missing (create-new opens the
17
+ * AdSense signup) and land it in omega.json5.
18
+ * Auth: GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET in the brand .env
19
+ * (adsense.readonly scope, own token cache).
20
+ */
21
+ const { serviceInputSpec } = require('../../config.js');
22
+ const { googleTokenStorePath } = require('../../lib/google-auth.js');
23
+ const { createServiceRunner } = require('../../lib/service-runner.js');
24
+ const { requestServiceInput } = require('../../lib/service-input.js');
25
+ const { GoogleAdsenseAPI } = require('./lib/adsense-api.js');
26
+ const { resolveConfigValue } = require('../../lib/config-flow.js');
27
+
28
+ const CREATE_ACCOUNT_URL = 'https://adsense.google.com/start/';
29
+
30
+ module.exports.run = createServiceRunner({
31
+ serviceDir: __dirname,
32
+ setup: async (context) => {
33
+ // No provider entry = deliberate absence — never resolve or write back an
34
+ // account the brand didn't opt into (wave-5 F10; authoring
35
+ // `advertising: { providers: { adsense: {} } }` opts in).
36
+ const provider = context.brandConfig.advertising?.providers?.adsense;
37
+ if (!provider) {
38
+ return { skip: true, reason: 'no advertising.providers.adsense section in omega.json5 (author it — even empty — to opt in)' };
39
+ }
40
+
41
+ const domain = (context.brandConfig.brand?.url || '').replace(/^https?:\/\//, '').replace(/\/$/, '');
42
+ if (!domain) {
43
+ return { skip: true, reason: 'no brand.url configured' };
44
+ }
45
+
46
+ // The shared setup contract (#608): ask for the Google OAuth client right
47
+ // here — provide, skip this run, or disable AdSense for good.
48
+ let gate = null;
49
+ if (!context.adsenseApi) {
50
+ gate = await requestServiceInput(context, serviceInputSpec('advertising'));
51
+ }
52
+ const haveCreds = Boolean(
53
+ context.adsenseApi
54
+ || (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET),
55
+ );
56
+
57
+ // Tests inject a fake client via context.adsenseApi
58
+ const makeApi = () => context.adsenseApi || new GoogleAdsenseAPI({
59
+ tokenStorePath: googleTokenStorePath(context.brandRoot),
60
+ });
61
+
62
+ // Missing client id → offer the interactive selection flow (lands in
63
+ // omega.json5); needs credentials. The Management API wants the bare
64
+ // pub-… account id — `client` carries the embed-ready ca-pub-… form.
65
+ let client = provider.client;
66
+ if (!client && haveCreds) {
67
+ const flowApi = makeApi();
68
+ client = await resolveConfigValue(context, {
69
+ path: 'advertising.providers.adsense.client',
70
+ // Disable must NOT land `client: false` — client is schema-typed as a
71
+ // string and the config would hard-fail validation forever. It opts
72
+ // the PROVIDER out instead (the chatsy/slapform shape): a falsy
73
+ // provider entry is the deliberate-absence skip above, and there is no
74
+ // second `enabled` switch to land it on since #527.
75
+ disablePath: 'advertising.providers.adsense',
76
+ label: 'AdSense account',
77
+ choices: () => flowApi.listAccounts(),
78
+ getName: (account) => {
79
+ const id = account.name.replace('accounts/', '');
80
+ return `${account.displayName || id} (${id})`;
81
+ },
82
+ getValue: (account) => `ca-${account.name.replace('accounts/', '')}`,
83
+ createNew: { label: 'account', url: CREATE_ACCOUNT_URL, refreshChoices: true },
84
+ });
85
+ }
86
+ if (!client) {
87
+ return { skip: true, reason: 'no advertising.providers.adsense.client configured (ca-pub-… from https://adsense.google.com → Settings → Account information — or rerun interactively)' };
88
+ }
89
+ const accountId = client.replace(/^ca-/, '');
90
+
91
+ if (!haveCreds) {
92
+ return gate || { skip: true, reason: 'no GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET configured (set them in the brand .env)' };
93
+ }
94
+
95
+ return {
96
+ adsenseApi: makeApi(),
97
+ accountId,
98
+ domain,
99
+ };
100
+ },
101
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Google AdSense Management API v2 client — read-only by design (the API has
3
+ * no site-management writes at all; the `adsense.readonly` scope makes that
4
+ * explicit). Named-method surface over the shared GoogleOAuth2Client so
5
+ * tests can fake it method-for-method.
6
+ *
7
+ * Auth: GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET in the brand .env; the ONE
8
+ * shared token store (GOOGLE_SCOPES union — one consent covers everything).
9
+ */
10
+ const { GoogleOAuth2Client, GOOGLE_SCOPES } = require('../../../lib/google-auth.js');
11
+
12
+ const ADSENSE_API_BASE = 'https://adsense.googleapis.com/v2';
13
+
14
+ class GoogleAdsenseAPI {
15
+ constructor(options = {}) {
16
+ this.auth = new GoogleOAuth2Client({
17
+ clientId: options.clientId || process.env.GOOGLE_CLIENT_ID,
18
+ clientSecret: options.clientSecret || process.env.GOOGLE_CLIENT_SECRET,
19
+ tokenStorePath: options.tokenStorePath,
20
+ scopes: GOOGLE_SCOPES,
21
+ });
22
+ }
23
+
24
+ async makeRequest(endpoint, options = {}) {
25
+ return this.auth.makeRequest(`${ADSENSE_API_BASE}${endpoint}`, options);
26
+ }
27
+
28
+ /** AdSense accounts visible to the authed user (name: 'accounts/pub-…') */
29
+ async listAccounts() {
30
+ const data = await this.makeRequest('/accounts');
31
+ return data.accounts || [];
32
+ }
33
+
34
+ /** accountId: 'pub-…' (the accounts/ prefix already stripped) */
35
+ async listSites(accountId) {
36
+ const data = await this.makeRequest(`/accounts/${accountId}/sites`);
37
+ return data.sites || [];
38
+ }
39
+ }
40
+
41
+ module.exports = { GoogleAdsenseAPI };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Report which AI providers this brand can call. The setup gate already
3
+ * acquired what was missing (or skipped the whole service), so by here the
4
+ * environment IS the answer — this names it, so the run record says which
5
+ * providers the backend has credentials for instead of leaving it implicit.
6
+ *
7
+ * Names only: a value never reaches the log.
8
+ */
9
+ const chalk = require('chalk').default;
10
+
11
+ const { serviceInputSpec } = require('../../../config.js');
12
+
13
+ module.exports = () => {
14
+ const configured = serviceInputSpec('ai').inputs
15
+ .filter((input) => process.env[input.name])
16
+ .map((input) => input.name);
17
+
18
+ console.log(` ${chalk.green('✓')} Configured: ${chalk.cyan(configured.join(', '))}`);
19
+
20
+ return { output: { configured } };
21
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * AI service (#639) — the ONE key per AI provider, asked once.
3
+ *
4
+ * It provisions nothing: there is no AI platform API to reconcile a brand
5
+ * against, only two credentials the backend needs before it can call OpenAI
6
+ * or Anthropic (contact inference, content + newsletter generation). The
7
+ * legacy split — a company-wide `OMEGA_*` key beside the brand's bare one —
8
+ * is gone: the bare name is the only name, and a company-wide value is simply
9
+ * the COMPANY layer of the .env cascade under that same name.
10
+ *
11
+ * So the whole service is the shared setup contract's gate (lib/service-input.js,
12
+ * the server/forms/chat/email shape): a brand that wants AI pastes the keys
13
+ * mid-walk, a brand that does not answers Disable once (`ai.enabled: false`)
14
+ * and is never asked again. Both keys are OPTIONAL in the registry
15
+ * (`gates: false`), so preflight never gates a run on them.
16
+ *
17
+ * It runs before the delivery lane: the keys must be in the brand .env before
18
+ * a target's runtime env composes from it.
19
+ */
20
+ const { serviceInputSpec } = require('../../config.js');
21
+ const { createServiceRunner } = require('../../lib/service-runner.js');
22
+ const { requestServiceInput } = require('../../lib/service-input.js');
23
+
24
+ module.exports.run = createServiceRunner({
25
+ serviceDir: __dirname,
26
+ setup: async (context) => {
27
+ if (context.brandConfig.ai?.enabled === false) {
28
+ return { skip: true, reason: 'ai.enabled = false' };
29
+ }
30
+
31
+ const gate = await requestServiceInput(context, serviceInputSpec('ai'));
32
+ if (gate) return gate;
33
+
34
+ return {};
35
+ },
36
+ });
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Ensure the GA4 property is linked to the brand's Firebase project.
3
+ *
4
+ * Runs after the firebase service so the project exists. A property holds at
5
+ * most one FirebaseLink, and a Firebase project links to at most one
6
+ * property — so when our project is linked to the WRONG property, it's
7
+ * unlinked there and linked here (config is the source of truth). Links may
8
+ * store the project NUMBER rather than the ID; the number comes from config
9
+ * (cloud.config.messagingSenderId IS the project number) — no second API
10
+ * client needed.
11
+ *
12
+ * omega-manager retried "link still propagating" failures with blind sleeps and
13
+ * a Firebase-API fallback; this one WAITS on the condition instead — an
14
+ * interactive run polls the create until GA releases the link it just deleted
15
+ * (skip allowed), and only a skipped or headless run reports warned (#662).
16
+ *
17
+ * Also normalizes the stream Firebase auto-creates when linking ("Web App",
18
+ * often with no URI) to the naming convention — diff-gated.
19
+ */
20
+ const chalk = require('chalk').default;
21
+ const { pollWithSpinner } = require('../../../vendor/devkit/flows.js');
22
+ const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
23
+
24
+ // GA holds a just-deleted link for a moment before the property is free
25
+ const LINK_RELEASE_INTERVAL_MS = 5000;
26
+
27
+ module.exports = async function ensureGoogleFirebaseLink(context) {
28
+ const { analyticsApi: api, propertyId, brandConfig, options = {} } = context;
29
+
30
+ // Same derivation as the cloud service: ONE home (#23) — the client web
31
+ // config names the project (cp100)
32
+ const projectId = brandConfig.cloud?.config?.projectId;
33
+
34
+ if (brandConfig.cloud?.shared === true) {
35
+ console.log(chalk.dim(' ⊘ Shared Firebase project — the link belongs to its owning brand'));
36
+ return {};
37
+ }
38
+
39
+ if (!projectId) {
40
+ console.log(chalk.dim(' ⊘ No cloud.config.projectId configured — nothing to link'));
41
+ return {};
42
+ }
43
+
44
+ // Links can reference the project by ID or number
45
+ const projectNumber = brandConfig.cloud?.config?.messagingSenderId || null;
46
+ const isOurProject = (linkedProject) => linkedProject === projectId
47
+ || (projectNumber && linkedProject === projectNumber);
48
+
49
+ // === READ: the configured property's link ===
50
+ const links = await api.listFirebaseLinks(propertyId);
51
+
52
+ if (links.length > 0) {
53
+ const linkedProject = (links[0].project || '').replace('projects/', '');
54
+
55
+ if (isOurProject(linkedProject)) {
56
+ console.log(` ${chalk.green('✓')} GA property linked to this Firebase project`);
57
+ const streamResult = await normalizeFirebaseStream(api, propertyId, brandConfig.brand.name, projectId, options);
58
+ return { output: { firebaseLink: { propertyId, linked: true, streamUpdated: streamResult } } };
59
+ }
60
+
61
+ // The property belongs to another project — that's not ours to break
62
+ console.log(` ${chalk.yellow('⚠')} GA property ${chalk.cyan(propertyId)} is linked to a DIFFERENT Firebase project: ${chalk.cyan(linkedProject)}`);
63
+ console.log(` ${chalk.dim('→')} Unlink it in GA Admin (or fix analytics.providers.google.propertyId), then rerun`);
64
+ return { status: 'warned', reason: `the GA property is linked to a different Firebase project (${linkedProject})`, output: { firebaseLink: { error: `property linked to different project: ${linkedProject}` } } };
65
+ }
66
+
67
+ // === No link on our property — find where (if anywhere) our project is linked ===
68
+ console.log(` ${chalk.dim('→')} No link on property ${chalk.cyan(propertyId)} — searching all GA properties...`);
69
+ const elsewhere = await findExistingLink(api, isOurProject, propertyId);
70
+
71
+ if (options.dryRun) {
72
+ const plannedActions = elsewhere
73
+ ? [`unlink from property ${elsewhere.propertyId}`, `link to property ${propertyId}`]
74
+ : [`link to property ${propertyId}`];
75
+ return dryRunPlan(`${plannedActions.join(', then ')}`, { output: { firebaseLink: { planned: plannedActions } } });
76
+ }
77
+
78
+ if (elsewhere) {
79
+ console.log(` ${chalk.yellow('⚠')} Firebase project is linked to the wrong GA property ${chalk.cyan(elsewhere.propertyId)} — unlinking...`);
80
+ await api.deleteFirebaseLink(elsewhere.propertyId, elsewhere.linkId);
81
+ console.log(` ${chalk.green('✓')} Unlinked from property ${chalk.cyan(elsewhere.propertyId)}`);
82
+ }
83
+
84
+ // === WRITE: create the link (a just-deleted link can take a moment to
85
+ // release — GA answers with a precondition failure; the create is retried
86
+ // in THIS walk rather than left to a rerun, #662) ===
87
+ console.log(` ${chalk.dim('→')} Linking GA property ${chalk.cyan(propertyId)} to Firebase project ${chalk.cyan(projectId)}...`);
88
+ let link;
89
+ try {
90
+ link = await api.createFirebaseLink(propertyId, projectId);
91
+ } catch (error) {
92
+ if (!isLinkNotReady(error)) {
93
+ throw error;
94
+ }
95
+
96
+ console.log(` ${chalk.yellow('⚠')} Link not ready yet${chalk.dim(`: ${error.message}`)}`);
97
+ link = canPrompt(options) ? await waitForLink(api, propertyId, projectId) : null;
98
+
99
+ if (!link) {
100
+ console.log(` ${chalk.dim('→')} Still not ready — rerun in a minute`);
101
+ return { status: 'warned', reason: 'the Firebase link is not ready yet — rerun in a minute', output: { firebaseLink: { error: error.message } } };
102
+ }
103
+ }
104
+
105
+ console.log(` ${chalk.green('✓')} Created Firebase link`);
106
+
107
+ const streamResult = await normalizeFirebaseStream(api, propertyId, brandConfig.brand.name, projectId, options);
108
+
109
+ return { output: { firebaseLink: { propertyId, linked: true, linkName: link.name || null, streamUpdated: streamResult } } };
110
+ };
111
+
112
+ /** GA's answer while a just-deleted link still holds the property. */
113
+ function isLinkNotReady(error) {
114
+ return error.message.includes('Precondition') || error.message.includes('already linked');
115
+ }
116
+
117
+ /**
118
+ * Retry the create until GA releases the deleted link (ENTER checks now, `s`
119
+ * skips). Any OTHER error is the caller's to throw — a poll must not turn a
120
+ * real failure into a wait.
121
+ *
122
+ * @returns {Promise<object|null>} - The created link, or null when the wait
123
+ * was skipped.
124
+ */
125
+ async function waitForLink(api, propertyId, projectId) {
126
+ let fatal = null;
127
+
128
+ const result = await pollWithSpinner({
129
+ check: async () => {
130
+ try {
131
+ return { done: true, result: await api.createFirebaseLink(propertyId, projectId) };
132
+ } catch (error) {
133
+ if (isLinkNotReady(error)) {
134
+ return { done: false };
135
+ }
136
+ fatal = error;
137
+ return { done: true, error: error.message };
138
+ }
139
+ },
140
+ intervalMs: LINK_RELEASE_INTERVAL_MS,
141
+ message: 'Waiting for GA to release the previous Firebase link',
142
+ indent: ' ',
143
+ });
144
+
145
+ if (fatal) {
146
+ throw fatal;
147
+ }
148
+
149
+ return result.success ? result.result : null;
150
+ }
151
+
152
+ /**
153
+ * Search every accessible account/property for a FirebaseLink pointing at
154
+ * our project (skipping the target property, already known to be linkless).
155
+ *
156
+ * @returns {Object|null} - { propertyId, linkId } or null
157
+ */
158
+ async function findExistingLink(api, isOurProject, targetPropertyId) {
159
+ const accounts = await api.listAccounts();
160
+
161
+ for (const account of accounts) {
162
+ const properties = await api.listProperties(account.name.replace('accounts/', ''));
163
+
164
+ for (const property of properties) {
165
+ const propId = property.name.replace('properties/', '');
166
+ if (propId === targetPropertyId) {
167
+ continue;
168
+ }
169
+
170
+ const links = await api.listFirebaseLinks(propId);
171
+ const ours = links.find((link) => isOurProject((link.project || '').replace('projects/', '')));
172
+ if (ours) {
173
+ console.log(` ${chalk.dim('→')} Found: linked to property ${chalk.cyan(propId)} (${property.displayName})`);
174
+ return { propertyId: propId, linkId: ours.name.split('/').pop() };
175
+ }
176
+ }
177
+ }
178
+
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Normalize the stream Firebase auto-creates when linking: rename "Web App"
184
+ * to "{Brand} - Firebase" and set the URI when missing. Diff-gated; a
185
+ * correct stream is untouched.
186
+ */
187
+ async function normalizeFirebaseStream(api, propertyId, brandName, projectId, options) {
188
+ const streams = await api.listDataStreams(propertyId);
189
+ const expectedName = `${brandName} - Firebase`;
190
+
191
+ const stream = streams.find((s) => {
192
+ const uri = s.webStreamData?.defaultUri || '';
193
+ return s.displayName === 'Web App'
194
+ || s.displayName === expectedName
195
+ || uri.includes('.firebaseapp.com')
196
+ || uri.includes('.web.app');
197
+ });
198
+
199
+ if (!stream) {
200
+ return null;
201
+ }
202
+
203
+ const streamId = stream.name.split('/').pop();
204
+ const needsRename = stream.displayName !== expectedName;
205
+ const needsUri = !stream.webStreamData?.defaultUri;
206
+
207
+ if (!needsRename && !needsUri) {
208
+ return { alreadyCorrect: true, streamId };
209
+ }
210
+
211
+ if (options.dryRun) {
212
+ return { planned: { renamed: needsRename, uriSet: needsUri }, streamId };
213
+ }
214
+
215
+ const updates = {};
216
+ const updateMaskFields = [];
217
+ if (needsRename) {
218
+ updates.displayName = expectedName;
219
+ updateMaskFields.push('displayName');
220
+ }
221
+ if (needsUri) {
222
+ updates.webStreamData = { defaultUri: `https://${projectId}.firebaseapp.com` };
223
+ updateMaskFields.push('webStreamData.defaultUri');
224
+ }
225
+
226
+ await api.updateDataStream(propertyId, streamId, updates, updateMaskFields);
227
+ console.log(` ${chalk.green('✓')} Normalized the Firebase stream${needsRename ? ` ("${stream.displayName}" → "${expectedName}")` : ''}`);
228
+
229
+ return { updated: true, streamId, renamed: needsRename, uriSet: needsUri };
230
+ }