@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.
- package/.claude-plugin/marketplace.json +14 -0
- package/LICENSE +98 -0
- package/README.md +165 -0
- package/bin/omega +2 -0
- package/bin/omg +2 -0
- package/claude-plugin/.claude-plugin/plugin.json +11 -0
- package/claude-plugin/.mcp.json +9 -0
- package/claude-plugin/README.md +139 -0
- package/claude-plugin/hooks/gate/mark.sh +58 -0
- package/claude-plugin/hooks/gate/run.sh +97 -0
- package/claude-plugin/hooks/guard/run.sh +249 -0
- package/claude-plugin/hooks/hooks.json +63 -0
- package/claude-plugin/hooks/inject/run.sh +127 -0
- package/claude-plugin/hooks/lib/omega-gate.sh +20 -0
- package/claude-plugin/hooks/lib/omega-scope.sh +41 -0
- package/claude-plugin/hooks/lib/omega-skills.sh +209 -0
- package/claude-plugin/hooks/quality/run.sh +132 -0
- package/claude-plugin/hooks/shape/run.sh +45 -0
- package/claude-plugin/mcp-router-launch.js +29 -0
- package/claude-plugin/skills/README.md +48 -0
- package/claude-plugin/skills/accessibility/SKILL.md +31 -0
- package/claude-plugin/skills/analytics/SKILL.md +30 -0
- package/claude-plugin/skills/backend/SKILL.md +25 -0
- package/claude-plugin/skills/brandcheck/SKILL.md +29 -0
- package/claude-plugin/skills/browser/SKILL.md +64 -0
- package/claude-plugin/skills/client/SKILL.md +24 -0
- package/claude-plugin/skills/desktop/SKILL.md +25 -0
- package/claude-plugin/skills/extension/SKILL.md +25 -0
- package/claude-plugin/skills/main/SKILL.md +54 -0
- package/claude-plugin/skills/manager/SKILL.md +25 -0
- package/claude-plugin/skills/seo/SKILL.md +36 -0
- package/claude-plugin/skills/theme/SKILL.md +33 -0
- package/claude-plugin/skills/web/SKILL.md +29 -0
- package/dist/cli-run.js +34 -0
- package/dist/cli.js +29 -0
- package/dist/commands/build.js +17 -0
- package/dist/commands/clean.js +16 -0
- package/dist/commands/company.js +46 -0
- package/dist/commands/deploy.js +143 -0
- package/dist/commands/dev.js +437 -0
- package/dist/commands/devlog.js +25 -0
- package/dist/commands/help.js +48 -0
- package/dist/commands/manage.js +53 -0
- package/dist/commands/migrate.js +76 -0
- package/dist/commands/onboard.js +31 -0
- package/dist/commands/pipeline.js +319 -0
- package/dist/commands/test.js +380 -0
- package/dist/commands/update.js +128 -0
- package/dist/commands/version.js +8 -0
- package/dist/company-init.js +167 -0
- package/dist/company.js +283 -0
- package/dist/config.js +1359 -0
- package/dist/devlog/index.js +226 -0
- package/dist/devlog/lib/collect-commits.js +136 -0
- package/dist/devlog/lib/generate-post.js +201 -0
- package/dist/devlog/lib/ghostii.js +122 -0
- package/dist/devlog/lib/project-map.js +98 -0
- package/dist/devlog/lib/publish-website.js +88 -0
- package/dist/index.js +45 -0
- package/dist/lib/agents-md.js +201 -0
- package/dist/lib/analytics-secret.js +22 -0
- package/dist/lib/argv.js +21 -0
- package/dist/lib/auth-admin.js +120 -0
- package/dist/lib/automation-client.js +210 -0
- package/dist/lib/backend-marketing.js +49 -0
- package/dist/lib/brand.js +240 -0
- package/dist/lib/bundle-id.js +53 -0
- package/dist/lib/claude-settings.js +125 -0
- package/dist/lib/company-scaffold.js +213 -0
- package/dist/lib/company.js +227 -0
- package/dist/lib/config-flow.js +350 -0
- package/dist/lib/config-write.js +41 -0
- package/dist/lib/custom-target.js +75 -0
- package/dist/lib/domain-utils.js +19 -0
- package/dist/lib/duration.js +39 -0
- package/dist/lib/env-order.js +236 -0
- package/dist/lib/env-secret.js +78 -0
- package/dist/lib/firestore-rest.js +326 -0
- package/dist/lib/framework-bin.js +156 -0
- package/dist/lib/gitignore.js +63 -0
- package/dist/lib/google-auth.js +495 -0
- package/dist/lib/google-token.js +66 -0
- package/dist/lib/jwt.js +31 -0
- package/dist/lib/legacy-oauth.js +59 -0
- package/dist/lib/node-version.js +127 -0
- package/dist/lib/owner-plan.js +78 -0
- package/dist/lib/package-scripts.js +152 -0
- package/dist/lib/preflight.js +470 -0
- package/dist/lib/product-create.js +144 -0
- package/dist/lib/run-command.js +74 -0
- package/dist/lib/run-gates.js +70 -0
- package/dist/lib/run-output.js +42 -0
- package/dist/lib/run-summary.js +384 -0
- package/dist/lib/scaffold.js +427 -0
- package/dist/lib/service-input.js +207 -0
- package/dist/lib/service-runner.js +466 -0
- package/dist/lib/stale.js +24 -0
- package/dist/lib/target-selection.js +155 -0
- package/dist/lib/verb-fanout.js +128 -0
- package/dist/lib/verify-live.js +188 -0
- package/dist/manage.js +311 -0
- package/dist/omega-bin.js +7 -0
- package/dist/onboard.js +532 -0
- package/dist/services/account/ensure/users.js +259 -0
- package/dist/services/account/index.js +97 -0
- package/dist/services/account/lib/backend-client.js +107 -0
- package/dist/services/account/lib/password.js +36 -0
- package/dist/services/account/lib/resolve-password.js +100 -0
- package/dist/services/advertising/ensure/sites.js +101 -0
- package/dist/services/advertising/index.js +101 -0
- package/dist/services/advertising/lib/adsense-api.js +41 -0
- package/dist/services/ai/ensure/keys.js +21 -0
- package/dist/services/ai/index.js +36 -0
- package/dist/services/analytics/ensure/google-firebase-link.js +230 -0
- package/dist/services/analytics/ensure/google-streams.js +256 -0
- package/dist/services/analytics/ensure/meta-pixel.js +22 -0
- package/dist/services/analytics/ensure/tiktok-pixel.js +22 -0
- package/dist/services/analytics/index.js +134 -0
- package/dist/services/analytics/lib/analytics-api.js +156 -0
- package/dist/services/analytics/lib/meta-api.js +83 -0
- package/dist/services/analytics/lib/pixel-account.js +78 -0
- package/dist/services/analytics/lib/pixel-provision.js +152 -0
- package/dist/services/analytics/lib/pixel-specs.js +57 -0
- package/dist/services/analytics/lib/pixel-token.js +88 -0
- package/dist/services/analytics/lib/property-flow.js +77 -0
- package/dist/services/analytics/lib/tiktok-api.js +157 -0
- package/dist/services/analytics/lib/tiktok-auth.js +153 -0
- package/dist/services/assets/ensure/logo-gen.js +68 -0
- package/dist/services/assets/index.js +109 -0
- package/dist/services/assets/lib/assets-config.js +168 -0
- package/dist/services/assets/lib/brandmark-api.js +160 -0
- package/dist/services/assets/lib/derived.js +113 -0
- package/dist/services/assets/lib/font-loader.js +67 -0
- package/dist/services/assets/lib/reconcile.js +122 -0
- package/dist/services/assets/lib/reset.js +118 -0
- package/dist/services/assets/lib/svg-logo-generator.js +154 -0
- package/dist/services/assets/lib/svg-to-black.js +32 -0
- package/dist/services/assets/write/favicons.js +82 -0
- package/dist/services/assets/write/icons.js +64 -0
- package/dist/services/assets/write/process.js +96 -0
- package/dist/services/assets/write/reconcile.js +30 -0
- package/dist/services/assets/write/social-icons.js +108 -0
- package/dist/services/assets/write/templates.js +345 -0
- package/dist/services/bookmark/ensure/sync.js +255 -0
- package/dist/services/bookmark/index.js +17 -0
- package/dist/services/campaigns/ensure/contact-person.js +40 -0
- package/dist/services/campaigns/ensure/custom-fields.js +84 -0
- package/dist/services/campaigns/ensure/domain-auth.js +92 -0
- package/dist/services/campaigns/ensure/event-webhook.js +89 -0
- package/dist/services/campaigns/ensure/link-branding.js +187 -0
- package/dist/services/campaigns/ensure/list.js +50 -0
- package/dist/services/campaigns/ensure/segments.js +109 -0
- package/dist/services/campaigns/ensure/sender-identity.js +153 -0
- package/dist/services/campaigns/ensure/unsubscribe-groups.js +116 -0
- package/dist/services/campaigns/index.js +79 -0
- package/dist/services/campaigns/lib/dns-sync.js +81 -0
- package/dist/services/campaigns/lib/segment-query.js +138 -0
- package/dist/services/campaigns/lib/sendgrid-api.js +287 -0
- package/dist/services/captcha/ensure/site-key.js +83 -0
- package/dist/services/captcha/index.js +78 -0
- package/dist/services/captcha/lib/console-url.js +45 -0
- package/dist/services/captcha/lib/recaptcha-api.js +36 -0
- package/dist/services/certificates/ensure/api-key.js +22 -0
- package/dist/services/certificates/ensure/bundle-ids.js +115 -0
- package/dist/services/certificates/ensure/certificates.js +187 -0
- package/dist/services/certificates/ensure/profiles.js +143 -0
- package/dist/services/certificates/index.js +225 -0
- package/dist/services/certificates/lib/apple-api.js +183 -0
- package/dist/services/certificates/lib/certificate-manager.js +252 -0
- package/dist/services/certificates/lib/identifier-manager.js +107 -0
- package/dist/services/certificates/lib/keychain.js +135 -0
- package/dist/services/certificates/lib/manual-walkthrough.js +165 -0
- package/dist/services/certificates/lib/profile-manager.js +115 -0
- package/dist/services/chat/data/baseline-knowledge.md +96 -0
- package/dist/services/chat/ensure/chat.js +106 -0
- package/dist/services/chat/ensure/user.js +45 -0
- package/dist/services/chat/index.js +139 -0
- package/dist/services/chat/lib/baseline-knowledge.js +137 -0
- package/dist/services/cloud/ensure/authentication.js +267 -0
- package/dist/services/cloud/ensure/billing.js +120 -0
- package/dist/services/cloud/ensure/cloud-messaging.js +105 -0
- package/dist/services/cloud/ensure/database.js +60 -0
- package/dist/services/cloud/ensure/firestore.js +79 -0
- package/dist/services/cloud/ensure/functions.js +49 -0
- package/dist/services/cloud/ensure/hosting.js +398 -0
- package/dist/services/cloud/ensure/oauth-consent.js +248 -0
- package/dist/services/cloud/ensure/project-settings.js +72 -0
- package/dist/services/cloud/ensure/sdk-config.js +95 -0
- package/dist/services/cloud/ensure/service-account.js +152 -0
- package/dist/services/cloud/ensure/services.js +103 -0
- package/dist/services/cloud/ensure/storage.js +47 -0
- package/dist/services/cloud/index.js +130 -0
- package/dist/services/cloud/lib/access-heal.js +130 -0
- package/dist/services/cloud/lib/firebase-api.js +662 -0
- package/dist/services/cloud/lib/project-flow.js +102 -0
- package/dist/services/directory/ensure/entry.js +48 -0
- package/dist/services/directory/index.js +64 -0
- package/dist/services/directory/lib/blocks.js +104 -0
- package/dist/services/disperse/index.js +45 -0
- package/dist/services/disperse/write/certs.js +117 -0
- package/dist/services/domain/ensure/nameservers.js +179 -0
- package/dist/services/domain/index.js +69 -0
- package/dist/services/domain/lib/namecheap-api.js +167 -0
- package/dist/services/domain/lib/registrars.js +46 -0
- package/dist/services/domain/lib/whitelist-walkthrough.js +76 -0
- package/dist/services/edge/ensure/cache-rules.js +105 -0
- package/dist/services/edge/ensure/dns-records.js +304 -0
- package/dist/services/edge/ensure/email-routing.js +292 -0
- package/dist/services/edge/ensure/rules-configuration.js +98 -0
- package/dist/services/edge/ensure/rules-managed-transforms.js +101 -0
- package/dist/services/edge/ensure/rules-redirect.js +107 -0
- package/dist/services/edge/ensure/rules-response-headers.js +90 -0
- package/dist/services/edge/ensure/rules-security.js +142 -0
- package/dist/services/edge/ensure/speed-scheduled-tests.js +98 -0
- package/dist/services/edge/ensure/workers.js +219 -0
- package/dist/services/edge/ensure/zone-settings.js +108 -0
- package/dist/services/edge/ensure/zone.js +183 -0
- package/dist/services/edge/index.js +73 -0
- package/dist/services/edge/lib/cloudflare-api.js +84 -0
- package/dist/services/edge/lib/dns-records-helpers.js +518 -0
- package/dist/services/edge/lib/read-cache.js +16 -0
- package/dist/services/edge/lib/ruleset-helper.js +108 -0
- package/dist/services/edge/workers/omega-api-proxy.js +43 -0
- package/dist/services/email/data/baseline-filter.md +4 -0
- package/dist/services/email/data/baseline-knowledge.md +86 -0
- package/dist/services/email/ensure/agent.js +107 -0
- package/dist/services/email/ensure/user.js +45 -0
- package/dist/services/email/index.js +138 -0
- package/dist/services/email/lib/baseline.js +82 -0
- package/dist/services/email/lib/knowledge-file.js +53 -0
- package/dist/services/forms/ensure/form.js +46 -0
- package/dist/services/forms/ensure/user.js +45 -0
- package/dist/services/forms/index.js +138 -0
- package/dist/services/migrations/ensure/notifications.js +189 -0
- package/dist/services/migrations/ensure/orders.js +34 -0
- package/dist/services/migrations/ensure/payment-provider.js +132 -0
- package/dist/services/migrations/ensure/payments-intents.js +34 -0
- package/dist/services/migrations/ensure/state-retirement.js +266 -0
- package/dist/services/migrations/ensure/targets-rename.js +124 -0
- package/dist/services/migrations/ensure/users.js +1176 -0
- package/dist/services/migrations/index.js +101 -0
- package/dist/services/migrations/lib/attribution-touch.js +64 -0
- package/dist/services/migrations/lib/ensure-metadata.js +144 -0
- package/dist/services/migrations/lib/migration-runner.js +473 -0
- package/dist/services/migrations/lib/sanitize-strings.js +112 -0
- package/dist/services/migrations/lib/schema-validator.js +154 -0
- package/dist/services/monitoring/ensure/dsn.js +54 -0
- package/dist/services/monitoring/ensure/projects.js +135 -0
- package/dist/services/monitoring/index.js +54 -0
- package/dist/services/monitoring/lib/sentry-api.js +94 -0
- package/dist/services/newsletter/ensure/custom-fields.js +90 -0
- package/dist/services/newsletter/ensure/publication.js +94 -0
- package/dist/services/newsletter/ensure/segments.js +140 -0
- package/dist/services/newsletter/ensure/webhook.js +108 -0
- package/dist/services/newsletter/index.js +64 -0
- package/dist/services/newsletter/lib/beehiiv-api.js +124 -0
- package/dist/services/newsletter/lib/segment-automation.js +577 -0
- package/dist/services/payment/ensure/chargebee-account.js +30 -0
- package/dist/services/payment/ensure/chargebee-products.js +359 -0
- package/dist/services/payment/ensure/chargebee-webhook.js +113 -0
- package/dist/services/payment/ensure/paypal-account.js +36 -0
- package/dist/services/payment/ensure/paypal-products.js +374 -0
- package/dist/services/payment/ensure/paypal-webhook.js +128 -0
- package/dist/services/payment/ensure/stripe-account.js +93 -0
- package/dist/services/payment/ensure/stripe-disputes.js +56 -0
- package/dist/services/payment/ensure/stripe-products.js +245 -0
- package/dist/services/payment/ensure/stripe-radar.js +74 -0
- package/dist/services/payment/ensure/stripe-webhook.js +130 -0
- package/dist/services/payment/index.js +163 -0
- package/dist/services/payment/lib/chargebee-api.js +338 -0
- package/dist/services/payment/lib/payment-utils.js +116 -0
- package/dist/services/payment/lib/paypal-api.js +354 -0
- package/dist/services/payment/lib/provider-setup.js +134 -0
- package/dist/services/payment/lib/stripe-api.js +152 -0
- package/dist/services/repo/ensure/org.js +70 -0
- package/dist/services/repo/ensure/pages.js +72 -0
- package/dist/services/repo/ensure/repo.js +90 -0
- package/dist/services/repo/index.js +52 -0
- package/dist/services/repo/lib/github-api.js +196 -0
- package/dist/services/search/ensure/ga-link.js +57 -0
- package/dist/services/search/ensure/property.js +137 -0
- package/dist/services/search/ensure/sitemaps.js +70 -0
- package/dist/services/search/index.js +61 -0
- package/dist/services/search/lib/search-console-api.js +92 -0
- package/dist/services/seo/ensure/github-repos.js +271 -0
- package/dist/services/seo/index.js +55 -0
- package/dist/services/seo/lib/gh-api.js +161 -0
- package/dist/services/seo/templates/developer-tool/.github/workflows/maintenance.yml +25 -0
- package/dist/services/seo/templates/developer-tool/.nvmrc +1 -0
- package/dist/services/seo/templates/developer-tool/_README.md.js +121 -0
- package/dist/services/seo/templates/developer-tool/_package.json.js +33 -0
- package/dist/services/seo/templates/developer-tool/src/index.js +120 -0
- package/dist/services/seo/templates/index.js +57 -0
- package/dist/services/server/ensure/brands.js +63 -0
- package/dist/services/server/index.js +42 -0
- package/dist/services/testing/ensure/target-checks.js +97 -0
- package/dist/services/testing/index.js +11 -0
- package/dist/services/testing/lib/checks.js +467 -0
- package/dist/services/update/index.js +20 -0
- package/dist/services/update/lib/cache.js +59 -0
- package/dist/services/update/lib/fingerprint.js +157 -0
- package/dist/services/update/write/targets.js +206 -0
- package/dist/services/workspace/ensure/agents.js +42 -0
- package/dist/services/workspace/ensure/claude-settings.js +34 -0
- package/dist/services/workspace/ensure/config.js +54 -0
- package/dist/services/workspace/ensure/defaults.js +60 -0
- package/dist/services/workspace/ensure/env-keys.js +55 -0
- package/dist/services/workspace/ensure/env-order.js +66 -0
- package/dist/services/workspace/ensure/env-rules.js +97 -0
- package/dist/services/workspace/ensure/gitignore.js +18 -0
- package/dist/services/workspace/ensure/scripts.js +126 -0
- package/dist/services/workspace/ensure/structure.js +97 -0
- package/dist/services/workspace/ensure/translation-sdk.js +148 -0
- package/dist/services/workspace/ensure/workflows.js +54 -0
- package/dist/services/workspace/index.js +8 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/certs.js +199 -0
- package/dist/vendor/devkit/ci-workflows.js +520 -0
- package/dist/vendor/devkit/cli-router.js +156 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/flows.js +317 -0
- package/dist/vendor/devkit/local-https.js +360 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/prompt.js +187 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/translate/cache.js +84 -0
- package/dist/vendor/devkit/translate/engine.js +243 -0
- package/dist/vendor/devkit/translate/index.js +49 -0
- package/dist/vendor/devkit/translate/languages.js +134 -0
- package/dist/vendor/devkit/translate/providers.js +188 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/docs/AGENTS.md +200 -0
- package/docs/account.md +45 -0
- package/docs/advertising.md +46 -0
- package/docs/ai.md +34 -0
- package/docs/analytics.md +69 -0
- package/docs/assets.md +57 -0
- package/docs/bookmark.md +26 -0
- package/docs/brand.md +151 -0
- package/docs/campaigns.md +96 -0
- package/docs/captcha.md +46 -0
- package/docs/certificates.md +68 -0
- package/docs/chat.md +47 -0
- package/docs/cloud.md +110 -0
- package/docs/company.md +64 -0
- package/docs/directory.md +140 -0
- package/docs/disperse.md +46 -0
- package/docs/domain.md +56 -0
- package/docs/edge.md +234 -0
- package/docs/email.md +42 -0
- package/docs/forms.md +48 -0
- package/docs/index.md +109 -0
- package/docs/migration.md +203 -0
- package/docs/migrations.md +56 -0
- package/docs/monitoring.md +41 -0
- package/docs/newsletter.md +48 -0
- package/docs/payment.md +83 -0
- package/docs/repo.md +48 -0
- package/docs/search.md +44 -0
- package/docs/seo.md +34 -0
- package/docs/server.md +37 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +34 -0
- package/docs/update.md +36 -0
- package/docs/workspace.md +64 -0
- 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
|
+
}
|