@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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search service (`search.providers.searchConsole`) — the brand's Search
|
|
3
|
+
* Console domain property (`sc-domain:{domain}` covers every subdomain),
|
|
4
|
+
* created and verified via a DNS TXT record written through Cloudflare, plus
|
|
5
|
+
* sitemap submission and the GA-association nudge.
|
|
6
|
+
*
|
|
7
|
+
* Runs after the edge service (the zone must exist for TXT verification) and
|
|
8
|
+
* after analytics (the GA property is what ga-link associates with).
|
|
9
|
+
*
|
|
10
|
+
* Auth: GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET in the brand .env (OAuth2;
|
|
11
|
+
* tokens cache to the ONE shared .omega/auth/google-tokens.json store
|
|
12
|
+
* (lib/google-auth.js) alongside the firebase and analytics grants — the
|
|
13
|
+
* webmasters + siteverification scopes ride the same file). No credentials →
|
|
14
|
+
* the service skips.
|
|
15
|
+
*/
|
|
16
|
+
const { serviceInputSpec } = require('../../config.js');
|
|
17
|
+
const { googleTokenStorePath } = require('../../lib/google-auth.js');
|
|
18
|
+
const { createServiceRunner } = require('../../lib/service-runner.js');
|
|
19
|
+
const { requestServiceInput } = require('../../lib/service-input.js');
|
|
20
|
+
const { CloudflareAPI } = require('../edge/lib/cloudflare-api.js');
|
|
21
|
+
const { getApexDomain } = require('../../lib/domain-utils.js');
|
|
22
|
+
const { GoogleSearchConsoleAPI } = require('./lib/search-console-api.js');
|
|
23
|
+
|
|
24
|
+
module.exports.run = createServiceRunner({
|
|
25
|
+
serviceDir: __dirname,
|
|
26
|
+
setup: async (context) => {
|
|
27
|
+
const searchConsole = context.brandConfig.search?.providers?.searchConsole || {};
|
|
28
|
+
|
|
29
|
+
if (searchConsole.enabled === false) {
|
|
30
|
+
return { skip: true, reason: 'search.providers.searchConsole.enabled = false' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const domain = (context.brandConfig.brand?.url || '').replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
34
|
+
if (!domain) {
|
|
35
|
+
return { skip: true, reason: 'no brand.url configured' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The shared setup contract (#608) — provide, skip this run, or disable
|
|
39
|
+
// the service for good; a headless run steps aside loudly.
|
|
40
|
+
if (!context.searchConsoleApi) {
|
|
41
|
+
const gate = await requestServiceInput(context, serviceInputSpec('search'));
|
|
42
|
+
if (gate) return gate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const apexDomain = getApexDomain(domain);
|
|
46
|
+
|
|
47
|
+
// Tests inject fake clients via context.searchConsoleApi / context.cloudflareApi.
|
|
48
|
+
// Cloudflare is only needed when the property still has to be DNS-verified;
|
|
49
|
+
// without a token the property operation prints the record to add manually.
|
|
50
|
+
return {
|
|
51
|
+
searchConsoleApi: context.searchConsoleApi || new GoogleSearchConsoleAPI({
|
|
52
|
+
tokenStorePath: googleTokenStorePath(context.brandRoot),
|
|
53
|
+
}),
|
|
54
|
+
cloudflareApi: context.cloudflareApi
|
|
55
|
+
|| (process.env.CLOUDFLARE_TOKEN ? new CloudflareAPI() : null),
|
|
56
|
+
domain,
|
|
57
|
+
apexDomain,
|
|
58
|
+
propertyUrl: `sc-domain:${domain}`,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Search Console client — the Webmasters v3 API (sites + sitemaps)
|
|
3
|
+
* plus the Site Verification v1 API (DNS_TXT domain verification). Named-
|
|
4
|
+
* method surface over the shared GoogleOAuth2Client so tests can fake it
|
|
5
|
+
* 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 SEARCH_CONSOLE_API_BASE = 'https://www.googleapis.com/webmasters/v3';
|
|
13
|
+
const SITE_VERIFICATION_API_BASE = 'https://www.googleapis.com/siteVerification/v1';
|
|
14
|
+
|
|
15
|
+
class GoogleSearchConsoleAPI {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.auth = new GoogleOAuth2Client({
|
|
18
|
+
clientId: options.clientId || process.env.GOOGLE_CLIENT_ID,
|
|
19
|
+
clientSecret: options.clientSecret || process.env.GOOGLE_CLIENT_SECRET,
|
|
20
|
+
tokenStorePath: options.tokenStorePath,
|
|
21
|
+
scopes: GOOGLE_SCOPES,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async makeRequest(endpoint, options = {}) {
|
|
26
|
+
return this.auth.makeRequest(`${SEARCH_CONSOLE_API_BASE}${endpoint}`, options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ========== Sites ==========
|
|
30
|
+
|
|
31
|
+
async listSites() {
|
|
32
|
+
const data = await this.makeRequest('/sites');
|
|
33
|
+
return data.siteEntry || [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** siteUrl: 'https://example.com/' or 'sc-domain:example.com' */
|
|
37
|
+
async addSite(siteUrl) {
|
|
38
|
+
await this.makeRequest(`/sites/${encodeURIComponent(siteUrl)}`, { method: 'PUT' });
|
|
39
|
+
return { added: true, siteUrl };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ========== Sitemaps ==========
|
|
43
|
+
|
|
44
|
+
async listSitemaps(siteUrl) {
|
|
45
|
+
const data = await this.makeRequest(`/sites/${encodeURIComponent(siteUrl)}/sitemaps`);
|
|
46
|
+
return data.sitemap || [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async submitSitemap(siteUrl, sitemapUrl) {
|
|
50
|
+
await this.makeRequest(
|
|
51
|
+
`/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(sitemapUrl)}`,
|
|
52
|
+
{ method: 'PUT' },
|
|
53
|
+
);
|
|
54
|
+
return { submitted: true, siteUrl, sitemapUrl };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ========== Site Verification ==========
|
|
58
|
+
|
|
59
|
+
async makeVerificationRequest(endpoint, options = {}) {
|
|
60
|
+
return this.auth.makeRequest(`${SITE_VERIFICATION_API_BASE}${endpoint}`, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the verification token for a site. Idempotent — the same token comes
|
|
65
|
+
* back for the same site+method until it's used.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} identifier - Domain (INET_DOMAIN) or URL (SITE)
|
|
68
|
+
* @param {string} verificationMethod - 'DNS_TXT' | 'DNS_CNAME' | 'FILE' | 'META'
|
|
69
|
+
* @param {string} type - 'INET_DOMAIN' (domain property) | 'SITE' (URL prefix)
|
|
70
|
+
*/
|
|
71
|
+
async getVerificationToken(identifier, verificationMethod = 'DNS_TXT', type = 'INET_DOMAIN') {
|
|
72
|
+
return await this.makeVerificationRequest('/token', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
site: { type, identifier },
|
|
76
|
+
verificationMethod,
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Claim ownership once the token is in place (method goes in the query). */
|
|
82
|
+
async verifySite(identifier, verificationMethod = 'DNS_TXT', type = 'INET_DOMAIN') {
|
|
83
|
+
return await this.makeVerificationRequest(`/webResource?verificationMethod=${verificationMethod}`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
site: { type, identifier },
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { GoogleSearchConsoleAPI };
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the brand's parasite SEO repos exist and match their template:
|
|
3
|
+
* create the repo if missing (public, auto-init README), push generated +
|
|
4
|
+
* static template files (content-compared — unchanged files are never
|
|
5
|
+
* rewritten), delete stale non-template files, reconcile description/
|
|
6
|
+
* homepage/topics, and star the repo as the author.
|
|
7
|
+
*
|
|
8
|
+
* Collision guardrail (critical safety, ported verbatim): a parasite repo
|
|
9
|
+
* this handler created only ever contains the template's files, so before
|
|
10
|
+
* ANY write the existing tree is inspected — more than MAX_STALE_FILES
|
|
11
|
+
* non-template files means the configured name collides with a REAL repo,
|
|
12
|
+
* and the item errors without touching anything. Never reuse a real
|
|
13
|
+
* repo's name for a parasite entry.
|
|
14
|
+
*
|
|
15
|
+
* Per-item author identity: author.token (`env:VAR` or literal) overrides
|
|
16
|
+
* the gh auth for that item's API calls; author.git rides the Contents
|
|
17
|
+
* API commits as author/committer.
|
|
18
|
+
*/
|
|
19
|
+
const chalk = require('chalk').default;
|
|
20
|
+
|
|
21
|
+
const { loadTemplate } = require('../templates/index.js');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maximum non-template ("stale") files tolerated before the handler
|
|
25
|
+
* refuses to manage the repo — the cap that stops a name collision from
|
|
26
|
+
* overwriting or wiping a real project. Legit parasite repos sit at 0–2
|
|
27
|
+
* stale files after a template change.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_STALE_FILES = 10;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve an author token config value (`env:VAR_NAME` reads process.env).
|
|
33
|
+
*/
|
|
34
|
+
function resolveToken(tokenConfig) {
|
|
35
|
+
if (!tokenConfig) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (tokenConfig.startsWith('env:')) {
|
|
40
|
+
const envVar = tokenConfig.slice(4);
|
|
41
|
+
const value = process.env[envVar];
|
|
42
|
+
|
|
43
|
+
if (!value) {
|
|
44
|
+
console.log(` ${chalk.yellow('⚠')} Token env var ${envVar} is not set — using default gh auth`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return tokenConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sleep(ms) {
|
|
55
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Poll for the repo after creation — `gh repo create` can silently create
|
|
60
|
+
* under the authenticated user when the owner is a user account the token
|
|
61
|
+
* doesn't control, so the caller fails loudly when this returns null.
|
|
62
|
+
*/
|
|
63
|
+
async function waitForRepo(api, slug, token, attempts = 3) {
|
|
64
|
+
for (let i = 0; i < attempts; i++) {
|
|
65
|
+
const repo = await api.getRepo(slug, token);
|
|
66
|
+
if (repo) {
|
|
67
|
+
return repo;
|
|
68
|
+
}
|
|
69
|
+
if (i < attempts - 1) {
|
|
70
|
+
await sleep(1500);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reconcile one content item to its repo. Returns the item status:
|
|
78
|
+
* 'created' | 'updated' | 'synced' | 'planned'.
|
|
79
|
+
*/
|
|
80
|
+
async function reconcileItem(api, slug, org, item, brandConfig, token, dryRun) {
|
|
81
|
+
let repo = await api.getRepo(slug, token);
|
|
82
|
+
let created = false;
|
|
83
|
+
|
|
84
|
+
if (!repo) {
|
|
85
|
+
const template = loadTemplate(item.template || 'developer-tool');
|
|
86
|
+
if (dryRun) {
|
|
87
|
+
console.log(` ${chalk.cyan('[DRY RUN]')} Would create repo + push ${template.files.length} files`);
|
|
88
|
+
return 'planned';
|
|
89
|
+
}
|
|
90
|
+
await api.createRepo(slug, item.description || '', token);
|
|
91
|
+
repo = await waitForRepo(api, slug, token);
|
|
92
|
+
if (!repo) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`repo not created at ${slug} — is "${org}" an org (or user) this token can create repos in? `
|
|
95
|
+
+ `it may have been created under the authenticated account instead`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
created = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const template = loadTemplate(item.template || 'developer-tool');
|
|
102
|
+
const data = { ...item, org, brand: brandConfig.brand };
|
|
103
|
+
const templatePaths = new Set(template.files.map((f) => f.path));
|
|
104
|
+
|
|
105
|
+
// Collision guard: inspect the tree BEFORE modifying anything
|
|
106
|
+
const tree = await api.getTree(slug, token);
|
|
107
|
+
const staleFiles = tree.filter((t) => t.type === 'blob' && !templatePaths.has(t.path));
|
|
108
|
+
|
|
109
|
+
if (staleFiles.length > MAX_STALE_FILES) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`refusing to manage: ${staleFiles.length} non-template files exceed cap of ${MAX_STALE_FILES} `
|
|
112
|
+
+ `— this looks like a real repo with a name collision, not a parasite repo we created`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let pushed = 0;
|
|
117
|
+
let unchanged = 0;
|
|
118
|
+
let deleted = 0;
|
|
119
|
+
let planned = 0;
|
|
120
|
+
|
|
121
|
+
// Push template files (content-compared; unchanged files skipped)
|
|
122
|
+
for (const file of template.files) {
|
|
123
|
+
const content = file.generate ? file.generate(data) : file.content;
|
|
124
|
+
|
|
125
|
+
const current = await api.getContents(slug, file.path, token);
|
|
126
|
+
if (current) {
|
|
127
|
+
const existing = Buffer.from(current.content, 'base64').toString();
|
|
128
|
+
if (existing === content) {
|
|
129
|
+
unchanged++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (dryRun) {
|
|
135
|
+
planned++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const body = {
|
|
140
|
+
message: current ? `Update ${file.path}` : `Add ${file.path}`,
|
|
141
|
+
content: Buffer.from(content).toString('base64'),
|
|
142
|
+
};
|
|
143
|
+
if (current) {
|
|
144
|
+
body.sha = current.sha;
|
|
145
|
+
}
|
|
146
|
+
if (item.author?.git) {
|
|
147
|
+
body.author = item.author.git;
|
|
148
|
+
body.committer = item.author.git;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await api.putContents(slug, file.path, body, token);
|
|
152
|
+
pushed++;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Delete the (few, guardrail-capped) stale files
|
|
156
|
+
for (const stale of staleFiles) {
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
planned++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const meta = await api.getContents(slug, stale.path, token);
|
|
163
|
+
const body = { message: `Remove ${stale.path}`, sha: meta.sha };
|
|
164
|
+
if (item.author?.git) {
|
|
165
|
+
body.author = item.author.git;
|
|
166
|
+
body.committer = item.author.git;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await api.deleteContents(slug, stale.path, body, token);
|
|
170
|
+
deleted++;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (dryRun && planned > 0) {
|
|
174
|
+
console.log(` ${chalk.cyan('[DRY RUN]')} Files: ${planned} would change, ${unchanged} unchanged`);
|
|
175
|
+
} else if (pushed + deleted > 0) {
|
|
176
|
+
console.log(` Files: ${pushed} pushed, ${unchanged} unchanged, ${deleted} deleted`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Repo settings — normalized compare (the API returns null for empty
|
|
180
|
+
// fields; omega-manager compared null !== '' and re-PATCHed every run)
|
|
181
|
+
const description = item.description || '';
|
|
182
|
+
const homepage = item.cta?.platform || item.cta?.url || brandConfig.brand?.url || '';
|
|
183
|
+
if ((repo.description || '') !== description || (repo.homepage || '') !== homepage) {
|
|
184
|
+
if (dryRun) {
|
|
185
|
+
console.log(` ${chalk.cyan('[DRY RUN]')} Would update description/homepage`);
|
|
186
|
+
planned++;
|
|
187
|
+
} else {
|
|
188
|
+
await api.patchRepo(slug, { description, homepage }, token);
|
|
189
|
+
pushed++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Topics — order-insensitive compare
|
|
194
|
+
if (item.topics?.length) {
|
|
195
|
+
const current = await api.getTopics(slug, token);
|
|
196
|
+
if ([...current].sort().join(',') !== [...item.topics].sort().join(',')) {
|
|
197
|
+
if (dryRun) {
|
|
198
|
+
console.log(` ${chalk.cyan('[DRY RUN]')} Would set topics: ${item.topics.join(', ')}`);
|
|
199
|
+
planned++;
|
|
200
|
+
} else {
|
|
201
|
+
await api.putTopics(slug, item.topics, token);
|
|
202
|
+
console.log(` Topics: ${item.topics.join(', ')}`);
|
|
203
|
+
pushed++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Star as the author (token) user
|
|
209
|
+
if (!(await api.isStarred(slug, token))) {
|
|
210
|
+
if (dryRun) {
|
|
211
|
+
planned++;
|
|
212
|
+
} else {
|
|
213
|
+
await api.starRepo(slug, token);
|
|
214
|
+
pushed++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (created) return 'created';
|
|
219
|
+
if (dryRun) return planned > 0 ? 'planned' : 'synced';
|
|
220
|
+
return pushed + deleted > 0 ? 'updated' : 'synced';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = async (context) => {
|
|
224
|
+
const { seoApi: api, seoContent: content, brandConfig } = context;
|
|
225
|
+
const dryRun = context.options?.dryRun || false;
|
|
226
|
+
|
|
227
|
+
const defaultOrg = brandConfig.repo?.providers?.github?.org;
|
|
228
|
+
const results = [];
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < content.length; i++) {
|
|
231
|
+
const item = content[i];
|
|
232
|
+
const org = item.org || defaultOrg;
|
|
233
|
+
const name = item.name;
|
|
234
|
+
|
|
235
|
+
if (!org || !name) {
|
|
236
|
+
console.log(` ${chalk.red('✗')} [${i + 1}/${content.length}] Missing org or name — skipping`);
|
|
237
|
+
results.push({ repo: `${org || '?'}/${name || '?'}`, status: 'error', error: 'missing org or name (set item.org or repo.providers.github.org)' });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const slug = `${org}/${name}`;
|
|
242
|
+
console.log(` [${i + 1}/${content.length}] ${chalk.cyan(slug)}`);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const token = resolveToken(item.author?.token);
|
|
246
|
+
const status = await reconcileItem(api, slug, org, item, brandConfig, token, dryRun);
|
|
247
|
+
console.log(` ${chalk.green('✓')} ${status}`);
|
|
248
|
+
results.push({ repo: slug, status });
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.log(` ${chalk.red('✗')} ${error.message}`);
|
|
251
|
+
results.push({ repo: slug, status: 'error', error: error.message });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const counts = {};
|
|
256
|
+
for (const result of results) {
|
|
257
|
+
counts[result.status] = (counts[result.status] || 0) + 1;
|
|
258
|
+
}
|
|
259
|
+
console.log(` Summary: ${results.length} repos — ${Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ')}`);
|
|
260
|
+
|
|
261
|
+
const failed = results.filter((r) => r.status === 'error');
|
|
262
|
+
const output = { githubRepos: { repos: results, ...counts } };
|
|
263
|
+
|
|
264
|
+
// Item failures surface as a service error (omega-manager swallowed them
|
|
265
|
+
// into a success) — items are independent, so the loop still ran them all
|
|
266
|
+
if (failed.length > 0) {
|
|
267
|
+
return { output, status: 'error', error: `${failed.length} repo(s) failed: ${failed.map((r) => r.repo).join(', ')}` };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { output };
|
|
271
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO service — parasite SEO content: programmatically created GitHub
|
|
3
|
+
* repos with templated READMEs, download scripts, and a maintenance
|
|
4
|
+
* workflow, defined per brand under seo.github.content[].
|
|
5
|
+
*
|
|
6
|
+
* Content lives in config/omega.json5 (`seo.github.content`) or the
|
|
7
|
+
* config/seo.json5 sidecar (the chatsy.md/replyify.md convention — big
|
|
8
|
+
* content sections in their own file; the sidecar's keys merge over the
|
|
9
|
+
* config's seo section). omega-manager kept the sidecar at
|
|
10
|
+
* .brands/{id}/seo.json and auto-created a default entry for every brand
|
|
11
|
+
* — the port never writes config (auto-creating entries is a parked onboarding follow-up); no
|
|
12
|
+
* content → clean skip.
|
|
13
|
+
*
|
|
14
|
+
* Auth: the default `gh` CLI auth, with per-item author overrides
|
|
15
|
+
* (author.token `env:VAR` + author.git identity) for parasite repos owned
|
|
16
|
+
* by separate accounts.
|
|
17
|
+
*/
|
|
18
|
+
const { join } = require('node:path');
|
|
19
|
+
const jetpack = require('fs-jetpack');
|
|
20
|
+
const JSON5 = require('json5');
|
|
21
|
+
|
|
22
|
+
const { createServiceRunner } = require('../../lib/service-runner.js');
|
|
23
|
+
const ghApi = require('./lib/gh-api.js');
|
|
24
|
+
|
|
25
|
+
module.exports.run = createServiceRunner({
|
|
26
|
+
serviceDir: __dirname,
|
|
27
|
+
setup: (context) => {
|
|
28
|
+
const config = context.brandConfig.seo;
|
|
29
|
+
|
|
30
|
+
if (config === false || config?.enabled === false) {
|
|
31
|
+
return { skip: true, reason: 'seo.enabled = false' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Sidecar keys merge over the config's seo section (omega-manager parity)
|
|
35
|
+
let seo = config || {};
|
|
36
|
+
const sidecarPath = join(context.brandRoot, 'config', 'seo.json5');
|
|
37
|
+
if (jetpack.exists(sidecarPath)) {
|
|
38
|
+
seo = { ...seo, ...JSON5.parse(jetpack.read(sidecarPath)) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!seo.github?.content?.length) {
|
|
42
|
+
return { skip: true, reason: 'no SEO content configured (add seo.github.content to config/omega.json5 or a config/seo.json5 sidecar)' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tests inject a fake API via context.seoApi; the real path needs gh
|
|
46
|
+
// installed + authenticated before any item work starts
|
|
47
|
+
let seoApi = context.seoApi;
|
|
48
|
+
if (!seoApi) {
|
|
49
|
+
ghApi.verifyGhCli();
|
|
50
|
+
seoApi = ghApi;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { seoApi, seoContent: seo.github.content };
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub operations for the seo service, over the `gh` CLI. Kept separate
|
|
3
|
+
* from the github service's GitHubAPI class because parasite repos need a
|
|
4
|
+
* per-item auth model: every method takes an optional token (the content
|
|
5
|
+
* item's author identity — a GH_TOKEN env override for that one call),
|
|
6
|
+
* falling back to the default `gh` auth. Tests fake this surface
|
|
7
|
+
* method-for-method via context.seoApi.
|
|
8
|
+
*/
|
|
9
|
+
const { execSync } = require('node:child_process');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build execSync options with an optional GH_TOKEN override.
|
|
13
|
+
*/
|
|
14
|
+
function ghOptions(token) {
|
|
15
|
+
const opts = { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
16
|
+
|
|
17
|
+
if (token) {
|
|
18
|
+
opts.env = { ...process.env, GH_TOKEN: token };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return opts;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function gh(args, token, input) {
|
|
25
|
+
const opts = ghOptions(token);
|
|
26
|
+
if (input !== undefined) {
|
|
27
|
+
opts.input = input;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return execSync(`gh ${args}`, opts).trim();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const stderr = error.stderr?.toString() || error.message;
|
|
33
|
+
throw new Error(`gh command failed: ${stderr.trim()}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ghJson(args, token, input) {
|
|
38
|
+
const result = gh(args, token, input);
|
|
39
|
+
return result ? JSON.parse(result) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Escape a string for use inside double quotes in a shell command.
|
|
44
|
+
*/
|
|
45
|
+
function shellEscape(str) {
|
|
46
|
+
return str.replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verify gh CLI is installed and authenticated (default auth — per-item
|
|
51
|
+
* tokens are validated by their own API calls).
|
|
52
|
+
*/
|
|
53
|
+
function verifyGhCli() {
|
|
54
|
+
try {
|
|
55
|
+
execSync('gh --version', { encoding: 'utf8', stdio: 'pipe' });
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error('GitHub CLI (gh) is not installed. Install from: https://cli.github.com/');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
execSync('gh auth status', { encoding: 'utf8', stdio: 'pipe' });
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error('GitHub CLI is not authenticated. Run: gh auth login (or set GH_TOKEN in the brand .env)');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Repo details, or null when it doesn't exist. */
|
|
68
|
+
function getRepo(slug, token) {
|
|
69
|
+
try {
|
|
70
|
+
return ghJson(`api repos/${slug}`, token);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error.message.includes('404')) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Create a public repo with an auto-init README. */
|
|
80
|
+
function createRepo(slug, description, token) {
|
|
81
|
+
const desc = description ? ` --description "${shellEscape(description)}"` : '';
|
|
82
|
+
gh(`repo create ${slug} --public --add-readme${desc}`, token);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Flat file tree of the main branch ([{ path, type }]); [] when unreadable (e.g. empty repo). */
|
|
86
|
+
function getTree(slug, token) {
|
|
87
|
+
try {
|
|
88
|
+
const tree = ghJson(`api repos/${slug}/git/trees/main?recursive=1`, token);
|
|
89
|
+
return tree?.tree || [];
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Contents-API file record ({ content: base64, sha }), or null when absent. */
|
|
96
|
+
function getContents(slug, path, token) {
|
|
97
|
+
try {
|
|
98
|
+
return ghJson(`api repos/${slug}/contents/${path}`, token);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.message.includes('404')) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Create/update a file via the Contents API (body: message, content, sha?, author?, committer?). */
|
|
108
|
+
function putContents(slug, path, body, token) {
|
|
109
|
+
return ghJson(`api repos/${slug}/contents/${path} -X PUT --input -`, token, JSON.stringify(body));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Delete a file via the Contents API (body: message, sha, author?, committer?). */
|
|
113
|
+
function deleteContents(slug, path, body, token) {
|
|
114
|
+
return ghJson(`api repos/${slug}/contents/${path} -X DELETE --input -`, token, JSON.stringify(body));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** PATCH repo settings from a JSON body (description, homepage, ...). */
|
|
118
|
+
function patchRepo(slug, fields, token) {
|
|
119
|
+
return ghJson(`api repos/${slug} -X PATCH --input -`, token, JSON.stringify(fields));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Current topic names. */
|
|
123
|
+
function getTopics(slug, token) {
|
|
124
|
+
const result = ghJson(`api repos/${slug}/topics`, token);
|
|
125
|
+
return result?.names || [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Replace the topic set. */
|
|
129
|
+
function putTopics(slug, names, token) {
|
|
130
|
+
return ghJson(`api repos/${slug}/topics -X PUT --input -`, token, JSON.stringify({ names }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Whether the authenticated (token) user has starred the repo. */
|
|
134
|
+
function isStarred(slug, token) {
|
|
135
|
+
try {
|
|
136
|
+
gh(`api user/starred/${slug} -X GET`, token);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Star the repo as the authenticated (token) user. */
|
|
144
|
+
function starRepo(slug, token) {
|
|
145
|
+
gh(`api user/starred/${slug} -X PUT`, token);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
verifyGhCli,
|
|
150
|
+
getRepo,
|
|
151
|
+
createRepo,
|
|
152
|
+
getTree,
|
|
153
|
+
getContents,
|
|
154
|
+
putContents,
|
|
155
|
+
deleteContents,
|
|
156
|
+
patchRepo,
|
|
157
|
+
getTopics,
|
|
158
|
+
putTopics,
|
|
159
|
+
isStarred,
|
|
160
|
+
starRepo,
|
|
161
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Maintenance
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: '0 1 * * *'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
ping:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout
|
|
16
|
+
uses: actions/checkout@v7
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
- name: Bump maintenance branch
|
|
20
|
+
run: |
|
|
21
|
+
git config user.name "GitHub Actions"
|
|
22
|
+
git config user.email "actions@github.com"
|
|
23
|
+
git checkout maintenance 2>/dev/null || git checkout -b maintenance
|
|
24
|
+
git commit --allow-empty --amend -m "Maintenance" || git commit --allow-empty -m "Maintenance"
|
|
25
|
+
git push --force origin maintenance
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
22
|