@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,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Runner — ported from omega-manager's migration-runner with the
|
|
3
|
+
* firebase-admin Firestore replaced by the shared FirestoreREST client.
|
|
4
|
+
*
|
|
5
|
+
* Abstracts the common pattern of iterating a collection, fixing docs, and
|
|
6
|
+
* validating. Handles stats tracking, logging, before/after snapshots
|
|
7
|
+
* (.omega/migrations/{collection}/{timestamp}/), and the summary.
|
|
8
|
+
*
|
|
9
|
+
* A run is an AUDIT by default (Ian 2026-08-20): every fix is computed,
|
|
10
|
+
* counted, logged and snapshotted, and Firestore is never touched. `--execute`
|
|
11
|
+
* is the only path that writes — nobody mutates a brand's collections by
|
|
12
|
+
* forgetting a flag.
|
|
13
|
+
*
|
|
14
|
+
* Fix functions receive (data, doc, db):
|
|
15
|
+
* - data: the plain document object (mutated in place as fixes apply)
|
|
16
|
+
* - doc: { id, createTime, updateTime } — server metadata (ISO strings,
|
|
17
|
+
* the REST stand-in for the admin SDK's DocumentSnapshot)
|
|
18
|
+
* - db: the FirestoreREST client (getDoc/patchDoc/deleteDoc/...)
|
|
19
|
+
* and return a dot-notation update map, null for no-op,
|
|
20
|
+
* { __delete__: true, reason } to delete the document, or updates carrying
|
|
21
|
+
* a `__logs__` array of per-field log lines.
|
|
22
|
+
*/
|
|
23
|
+
const { join } = require('node:path');
|
|
24
|
+
const jetpack = require('fs-jetpack');
|
|
25
|
+
const chalk = require('chalk').default;
|
|
26
|
+
|
|
27
|
+
// Field-deletion sentinel — replaces firebase-admin's FieldValue.delete().
|
|
28
|
+
// In a REST patch, a path in the updateMask with no value in the body IS the
|
|
29
|
+
// delete, so the sentinel only has to be identity-comparable.
|
|
30
|
+
const DELETE = Symbol('FieldValue.delete');
|
|
31
|
+
const FieldValue = { delete: () => DELETE };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Iterate a collection in pages via the REST client.
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} firestore - FirestoreREST client
|
|
37
|
+
* @param {string} collectionPath - Collection to iterate
|
|
38
|
+
* @param {Function} callback - Async (docs, batch, total) per page
|
|
39
|
+
* @param {Object} options - { batchSize, maxBatches }
|
|
40
|
+
*/
|
|
41
|
+
async function iterateCollection(firestore, collectionPath, callback, options = {}) {
|
|
42
|
+
const batchSize = options.batchSize || 500;
|
|
43
|
+
const maxBatches = options.maxBatches || Infinity;
|
|
44
|
+
|
|
45
|
+
const totalCount = await firestore.countDocs(collectionPath);
|
|
46
|
+
console.log(` ${chalk.dim('→')} ${chalk.cyan(totalCount)} documents in ${chalk.cyan(collectionPath)}`);
|
|
47
|
+
|
|
48
|
+
let batch = 0;
|
|
49
|
+
let pageToken = null;
|
|
50
|
+
|
|
51
|
+
while (true) {
|
|
52
|
+
const page = await firestore.listDocs(collectionPath, { pageSize: batchSize, pageToken });
|
|
53
|
+
console.log(chalk.dim(` Batch ${chalk.cyan(batch + 1)}: ${chalk.cyan(page.docs.length)} documents`));
|
|
54
|
+
|
|
55
|
+
if (page.docs.length === 0) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await callback(page.docs, batch, totalCount);
|
|
60
|
+
|
|
61
|
+
batch++;
|
|
62
|
+
pageToken = page.nextPageToken;
|
|
63
|
+
|
|
64
|
+
if (!pageToken || batch >= maxBatches) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Split a merged dot-notation update map into the REST patch pieces: the
|
|
72
|
+
* updateMask field paths (sets AND deletes) and the nested body object
|
|
73
|
+
* (sets only — a masked path absent from the body is a delete).
|
|
74
|
+
*/
|
|
75
|
+
function buildPatch(mergedUpdates) {
|
|
76
|
+
const fieldPaths = Object.keys(mergedUpdates);
|
|
77
|
+
const sets = {};
|
|
78
|
+
|
|
79
|
+
for (const [path, value] of Object.entries(mergedUpdates)) {
|
|
80
|
+
if (value === DELETE) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const parts = path.split('.');
|
|
84
|
+
let target = sets;
|
|
85
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
86
|
+
if (target[parts[i]] === undefined) {
|
|
87
|
+
target[parts[i]] = {};
|
|
88
|
+
}
|
|
89
|
+
target = target[parts[i]];
|
|
90
|
+
}
|
|
91
|
+
target[parts[parts.length - 1]] = value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { fieldPaths, sets };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run a migration over a collection.
|
|
99
|
+
*
|
|
100
|
+
* @param {Object} context - Handler context (brandId, brandRoot, firestore, options)
|
|
101
|
+
* @param {Object} options - Migration options
|
|
102
|
+
* @param {string} options.collection - Collection path to iterate
|
|
103
|
+
* @param {Array<Function>} options.fixes - Fix functions: (data, doc, db) => updates object or null
|
|
104
|
+
* @param {Function} options.validate - Validate function: (data, docId) => { valid, errors }
|
|
105
|
+
* @param {number} options.batchSize - Batch size (default: 500)
|
|
106
|
+
* @returns {Object} { output: { [collection]: stats }, status?, error? }
|
|
107
|
+
*/
|
|
108
|
+
async function runMigration(context, options) {
|
|
109
|
+
const { brandRoot, firestore } = context;
|
|
110
|
+
const { collection, fixes = [], validate } = options;
|
|
111
|
+
// The manage-wide --dry-run vetoes --execute: passing both means "don't write"
|
|
112
|
+
const execute = (context.options?.execute && !context.options?.dryRun) || false;
|
|
113
|
+
const verbose = context.options?.verbose || false;
|
|
114
|
+
const limit = context.options?.limit || 0;
|
|
115
|
+
const ids = context.options?.ids ? String(context.options.ids).split(',').map((id) => id.trim()) : null;
|
|
116
|
+
const batchSize = limit ? Math.min(options.batchSize || 500, limit) : (options.batchSize || 500);
|
|
117
|
+
|
|
118
|
+
const vetoed = context.options?.execute && context.options?.dryRun;
|
|
119
|
+
const modeLabel = execute
|
|
120
|
+
? chalk.red(' [EXECUTE]')
|
|
121
|
+
: chalk.yellow(vetoed ? ' [AUDIT — --dry-run vetoes --execute]' : ' [AUDIT — pass --execute to write]');
|
|
122
|
+
const limitLabel = limit ? chalk.dim(` (limit: ${chalk.cyan(limit)})`) : '';
|
|
123
|
+
const idsLabel = ids ? chalk.dim(` (${chalk.cyan(ids.length)} specific IDs)`) : '';
|
|
124
|
+
console.log(` ${chalk.dim('→')} Processing ${chalk.cyan(collection)} for ${chalk.cyan(firestore.projectId)}...${modeLabel}${limitLabel}${idsLabel}`);
|
|
125
|
+
|
|
126
|
+
// Track stats
|
|
127
|
+
const stats = {
|
|
128
|
+
totalDocs: 0,
|
|
129
|
+
validDocs: 0,
|
|
130
|
+
invalidDocs: 0,
|
|
131
|
+
docsFixed: 0,
|
|
132
|
+
docsDeleted: 0,
|
|
133
|
+
errors: 0,
|
|
134
|
+
invalidDocIds: [],
|
|
135
|
+
errorDocIds: [],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Snapshot directory for before/after records
|
|
139
|
+
const snapshotDir = join(brandRoot, '.omega', 'migrations', collection,
|
|
140
|
+
new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''));
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// If specific IDs provided, fetch just those docs instead of iterating the whole collection
|
|
144
|
+
const iterateFn = ids
|
|
145
|
+
? async (callback) => {
|
|
146
|
+
const docs = [];
|
|
147
|
+
for (const id of ids) {
|
|
148
|
+
const doc = await firestore.getDocWithMeta(`${collection}/${id}`);
|
|
149
|
+
if (doc) {
|
|
150
|
+
docs.push(doc);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.yellow(`Document not found: ${id}`)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (docs.length > 0) {
|
|
156
|
+
await callback(docs, 1, docs.length);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
: (callback) => iterateCollection(firestore, collection, callback, { batchSize, maxBatches: limit ? 1 : Infinity });
|
|
160
|
+
|
|
161
|
+
await iterateFn(async (docs, batch, total) => {
|
|
162
|
+
// Apply limit within the batch
|
|
163
|
+
const docsToProcess = limit ? docs.slice(0, limit - stats.totalDocs) : docs;
|
|
164
|
+
|
|
165
|
+
// Process all docs in the batch concurrently
|
|
166
|
+
const results = await Promise.all(docsToProcess.map(async (doc) => {
|
|
167
|
+
const data = doc.data;
|
|
168
|
+
const before = JSON.parse(JSON.stringify(data));
|
|
169
|
+
const logs = [];
|
|
170
|
+
const errorMessages = [];
|
|
171
|
+
let docWasFixed = false;
|
|
172
|
+
let docErrors = 0;
|
|
173
|
+
|
|
174
|
+
// Collect all updates from fixes into a single merged object
|
|
175
|
+
const mergedUpdates = {};
|
|
176
|
+
let docDeleted = false;
|
|
177
|
+
|
|
178
|
+
// Run fix functions sequentially (fixes depend on each other)
|
|
179
|
+
for (const fix of fixes) {
|
|
180
|
+
try {
|
|
181
|
+
const updates = await fix(data, doc, firestore);
|
|
182
|
+
if (!updates) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Handle document deletion
|
|
187
|
+
if (updates.__delete__) {
|
|
188
|
+
docDeleted = true;
|
|
189
|
+
const reason = updates.reason || 'unknown';
|
|
190
|
+
|
|
191
|
+
if (execute) {
|
|
192
|
+
await firestore.deleteDoc(`${collection}/${doc.id}`);
|
|
193
|
+
logs.push(` ${chalk.red('🗑')} ${chalk.red(`Deleted document (${chalk.cyan(reason)})`)}`);
|
|
194
|
+
} else {
|
|
195
|
+
logs.push(` ${chalk.yellow('~')} ${chalk.yellow(`Would delete document (${chalk.cyan(reason)})`)}`);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Extract sentinel: per-field log lines to render under this doc's header
|
|
201
|
+
const extraLogs = Array.isArray(updates.__logs__) ? updates.__logs__ : null;
|
|
202
|
+
if (extraLogs) {
|
|
203
|
+
delete updates.__logs__;
|
|
204
|
+
logs.push(...extraLogs);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Object.keys(updates).length === 0) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
docWasFixed = true;
|
|
212
|
+
Object.assign(mergedUpdates, updates);
|
|
213
|
+
|
|
214
|
+
// Log what's being fixed
|
|
215
|
+
const fixedFields = Object.keys(updates).filter((k) => updates[k] !== DELETE);
|
|
216
|
+
const deletedFields = Object.keys(updates).filter((k) => updates[k] === DELETE);
|
|
217
|
+
const desc = [...fixedFields.map((f) => `+${f}`), ...deletedFields.map((f) => `-${f}`)].join(', ');
|
|
218
|
+
|
|
219
|
+
if (execute) {
|
|
220
|
+
logs.push(` ${chalk.green('✓')} ${chalk.green(`Fixed (${chalk.cyan(desc)})`)}`);
|
|
221
|
+
} else {
|
|
222
|
+
logs.push(` ${chalk.yellow('~')} ${chalk.yellow(`Would fix (${chalk.cyan(desc)})`)}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Apply updates to local data for subsequent fixes and validation
|
|
226
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
227
|
+
const parts = key.split('.');
|
|
228
|
+
if (parts.length === 1) {
|
|
229
|
+
if (value === DELETE) {
|
|
230
|
+
delete data[key];
|
|
231
|
+
} else {
|
|
232
|
+
data[key] = value;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
const last = parts.pop();
|
|
236
|
+
let target = data;
|
|
237
|
+
for (const part of parts) {
|
|
238
|
+
if (target[part] === undefined) {
|
|
239
|
+
target[part] = {};
|
|
240
|
+
}
|
|
241
|
+
target = target[part];
|
|
242
|
+
}
|
|
243
|
+
if (value === DELETE) {
|
|
244
|
+
delete target[last];
|
|
245
|
+
} else {
|
|
246
|
+
target[last] = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
docErrors++;
|
|
252
|
+
errorMessages.push(`Fix failed: ${error.message}`);
|
|
253
|
+
logs.push(` ${chalk.red('❌')} ${chalk.red('Fix failed')}${chalk.dim(`: ${error.message}`)}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Resolve conflicts: if an ancestor path and a descendant path both exist
|
|
258
|
+
// in the same update (e.g. `metadata.created` + `metadata.created.timestamp`,
|
|
259
|
+
// or `activity` + `activity.foo.bar`), merge the descendants into the
|
|
260
|
+
// ancestor's object value. Firestore rejects updates that specify the same
|
|
261
|
+
// field via overlapping paths.
|
|
262
|
+
//
|
|
263
|
+
// Process ancestors from shortest to longest so that longer ancestors absorb
|
|
264
|
+
// their descendants before being absorbed themselves.
|
|
265
|
+
if (docWasFixed) {
|
|
266
|
+
const ancestorKeys = Object.keys(mergedUpdates)
|
|
267
|
+
.filter((k) => {
|
|
268
|
+
const value = mergedUpdates[k];
|
|
269
|
+
return value !== DELETE
|
|
270
|
+
&& value !== null
|
|
271
|
+
&& typeof value === 'object'
|
|
272
|
+
&& !Array.isArray(value);
|
|
273
|
+
})
|
|
274
|
+
.sort((a, b) => a.split('.').length - b.split('.').length);
|
|
275
|
+
|
|
276
|
+
for (const ancestorKey of ancestorKeys) {
|
|
277
|
+
if (!(ancestorKey in mergedUpdates)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const descendantKeys = Object.keys(mergedUpdates).filter((k) => k.startsWith(`${ancestorKey}.`));
|
|
282
|
+
for (const descKey of descendantKeys) {
|
|
283
|
+
const subParts = descKey.substring(ancestorKey.length + 1).split('.');
|
|
284
|
+
let target = mergedUpdates[ancestorKey];
|
|
285
|
+
for (let i = 0; i < subParts.length - 1; i++) {
|
|
286
|
+
if (target[subParts[i]] === undefined) {
|
|
287
|
+
target[subParts[i]] = {};
|
|
288
|
+
}
|
|
289
|
+
target = target[subParts[i]];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// A delete can't be nested inside a set object — just drop the key
|
|
293
|
+
if (mergedUpdates[descKey] === DELETE) {
|
|
294
|
+
delete target[subParts[subParts.length - 1]];
|
|
295
|
+
} else {
|
|
296
|
+
target[subParts[subParts.length - 1]] = mergedUpdates[descKey];
|
|
297
|
+
}
|
|
298
|
+
delete mergedUpdates[descKey];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// If doc was deleted, skip write/validate
|
|
304
|
+
if (docDeleted) {
|
|
305
|
+
jetpack.write(join(snapshotDir, `${doc.id}.json`), {
|
|
306
|
+
before,
|
|
307
|
+
after: null,
|
|
308
|
+
deleted: true,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return { doc, before, logs, errorMessages, docWasFixed: false, docDeleted, isValid: null, docErrors };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Write all fixes in a single Firestore call
|
|
315
|
+
if (docWasFixed && execute && Object.keys(mergedUpdates).length > 0) {
|
|
316
|
+
try {
|
|
317
|
+
const { fieldPaths, sets } = buildPatch(mergedUpdates);
|
|
318
|
+
await firestore.patchDoc(`${collection}/${doc.id}`, sets, fieldPaths);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
docErrors++;
|
|
321
|
+
errorMessages.push(`Write failed: ${error.message}`);
|
|
322
|
+
logs.push(` ${chalk.red('❌')} ${chalk.red('Write failed')}${chalk.dim(`: ${error.message}`)}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Run validate function if provided
|
|
327
|
+
let isValid = null;
|
|
328
|
+
if (validate) {
|
|
329
|
+
const result = await validate(data, doc.id);
|
|
330
|
+
isValid = result.valid;
|
|
331
|
+
|
|
332
|
+
if (result.valid) {
|
|
333
|
+
logs.push(` ${chalk.green('✓')} ${chalk.green('Valid')}`);
|
|
334
|
+
} else {
|
|
335
|
+
for (const error of result.errors) {
|
|
336
|
+
logs.push(` ${chalk.red('❌')} ${chalk.red(error.field)}${chalk.dim(`: ${error.message}`)}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Save before/after snapshot
|
|
342
|
+
jetpack.write(join(snapshotDir, `${doc.id}.json`), {
|
|
343
|
+
before,
|
|
344
|
+
after: data,
|
|
345
|
+
fixed: docWasFixed,
|
|
346
|
+
valid: isValid,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return { doc, before, logs, errorMessages, docWasFixed, docDeleted, isValid, docErrors };
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
// Flush logs and update stats
|
|
353
|
+
for (const { doc, before, logs, errorMessages, docWasFixed, docDeleted, isValid, docErrors } of results) {
|
|
354
|
+
stats.totalDocs++;
|
|
355
|
+
|
|
356
|
+
// Only log docs with fixes, errors, deletions, or invalid — skip "Valid" only docs unless verbose
|
|
357
|
+
const hasActivity = docWasFixed || docDeleted || docErrors > 0 || isValid === false;
|
|
358
|
+
if (hasActivity || verbose) {
|
|
359
|
+
console.log(chalk.cyan(` [${stats.totalDocs}/${total}] ${doc.id}`));
|
|
360
|
+
if (verbose) {
|
|
361
|
+
console.log(chalk.dim(JSON.stringify(before, null, 2).split('\n').map((l) => ` ${l}`).join('\n')));
|
|
362
|
+
}
|
|
363
|
+
for (const log of logs) {
|
|
364
|
+
console.log(log);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
stats.errors += docErrors;
|
|
369
|
+
if (docErrors > 0) {
|
|
370
|
+
stats.errorDocIds.push({ id: doc.id, errors: errorMessages });
|
|
371
|
+
}
|
|
372
|
+
if (docDeleted) {
|
|
373
|
+
stats.docsDeleted++;
|
|
374
|
+
} else if (docWasFixed) {
|
|
375
|
+
stats.docsFixed++;
|
|
376
|
+
}
|
|
377
|
+
if (isValid === true) {
|
|
378
|
+
stats.validDocs++;
|
|
379
|
+
} else if (isValid === false) {
|
|
380
|
+
stats.invalidDocs++;
|
|
381
|
+
stats.invalidDocIds.push(doc.id);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Progress log every batch
|
|
386
|
+
console.log(chalk.dim(` Processed ${chalk.cyan(`${stats.totalDocs}/${total}`)} documents...`));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Save summary.json
|
|
390
|
+
const summaryPath = join(snapshotDir, '_summary.json');
|
|
391
|
+
const summary = {
|
|
392
|
+
collection,
|
|
393
|
+
brandId: context.brandId,
|
|
394
|
+
projectId: firestore.projectId,
|
|
395
|
+
execute,
|
|
396
|
+
timestamp: new Date().toISOString(),
|
|
397
|
+
stats: {
|
|
398
|
+
totalDocs: stats.totalDocs,
|
|
399
|
+
validDocs: validate ? stats.validDocs : undefined,
|
|
400
|
+
invalidDocs: validate ? stats.invalidDocs : undefined,
|
|
401
|
+
docsFixed: fixes.length > 0 ? stats.docsFixed : undefined,
|
|
402
|
+
docsDeleted: stats.docsDeleted > 0 ? stats.docsDeleted : undefined,
|
|
403
|
+
errors: stats.errors > 0 ? stats.errors : undefined,
|
|
404
|
+
},
|
|
405
|
+
errorDocIds: stats.errorDocIds.length > 0 ? stats.errorDocIds : undefined,
|
|
406
|
+
invalidDocIds: stats.invalidDocIds.length > 0 ? stats.invalidDocIds : undefined,
|
|
407
|
+
};
|
|
408
|
+
jetpack.write(summaryPath, summary);
|
|
409
|
+
|
|
410
|
+
// Console summary
|
|
411
|
+
const parts = [`${chalk.bold(stats.totalDocs)} total`];
|
|
412
|
+
if (stats.docsFixed > 0) {
|
|
413
|
+
parts.push(`${chalk.green(chalk.bold(stats.docsFixed))} fixed`);
|
|
414
|
+
}
|
|
415
|
+
if (stats.docsDeleted > 0) {
|
|
416
|
+
parts.push(`${chalk.red(chalk.bold(stats.docsDeleted))} deleted`);
|
|
417
|
+
}
|
|
418
|
+
if (validate) {
|
|
419
|
+
parts.push(`${chalk.green(chalk.bold(stats.validDocs))} valid`);
|
|
420
|
+
}
|
|
421
|
+
if (stats.invalidDocs > 0) {
|
|
422
|
+
parts.push(`${chalk.red(chalk.bold(stats.invalidDocs))} invalid`);
|
|
423
|
+
}
|
|
424
|
+
if (stats.errors > 0) {
|
|
425
|
+
parts.push(`${chalk.red(chalk.bold(stats.errors))} errors`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const hasIssues = stats.errors > 0 || stats.invalidDocs > 0;
|
|
429
|
+
const icon = hasIssues ? chalk.yellow('⚠') : chalk.green('✓');
|
|
430
|
+
console.log(` ${icon} ${parts.join(', ')}`);
|
|
431
|
+
console.log(` ${chalk.dim(`Log: ${summaryPath}`)}`);
|
|
432
|
+
|
|
433
|
+
// Show retry hint if there are errors
|
|
434
|
+
if (stats.errorDocIds.length > 0) {
|
|
435
|
+
const errorIds = stats.errorDocIds.map((e) => e.id).join(',');
|
|
436
|
+
console.log(` ${chalk.yellow('Retry failed:')} --ids=${errorIds}`);
|
|
437
|
+
}
|
|
438
|
+
if (stats.invalidDocIds.length > 0) {
|
|
439
|
+
const invalidIds = stats.invalidDocIds.join(',');
|
|
440
|
+
console.log(` ${chalk.yellow('Review invalid:')} --ids=${invalidIds}`);
|
|
441
|
+
}
|
|
442
|
+
console.log('');
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.log(` ${chalk.red('❌')} ${chalk.red('Error during migration')}${chalk.dim(`: ${error.message}`)}`);
|
|
445
|
+
return { status: 'error', error: error.message };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Data issues (fix/write failures, schema-invalid docs) surface as warned —
|
|
449
|
+
// omega-manager printed the ⚠ but swallowed it into a success status
|
|
450
|
+
const result = {
|
|
451
|
+
output: {
|
|
452
|
+
[collection]: {
|
|
453
|
+
totalDocs: stats.totalDocs,
|
|
454
|
+
validDocs: validate ? stats.validDocs : undefined,
|
|
455
|
+
invalidDocs: validate ? stats.invalidDocs : undefined,
|
|
456
|
+
docsFixed: fixes.length > 0 ? stats.docsFixed : undefined,
|
|
457
|
+
docsDeleted: stats.docsDeleted > 0 ? stats.docsDeleted : undefined,
|
|
458
|
+
errors: stats.errors > 0 ? stats.errors : undefined,
|
|
459
|
+
errorDocIds: stats.errorDocIds.length > 0 ? stats.errorDocIds : undefined,
|
|
460
|
+
invalidDocIds: stats.invalidDocs > 0 ? stats.invalidDocIds : undefined,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (stats.errors > 0 || stats.invalidDocs > 0) {
|
|
466
|
+
result.status = 'warned';
|
|
467
|
+
result.reason = `${stats.errors} doc(s) errored, ${stats.invalidDocs} invalid`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = { runMigration, FieldValue };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whitespace-trim fix for migration documents.
|
|
3
|
+
*
|
|
4
|
+
* Historically omega-manager stripped HTML via sanitize-html, but that
|
|
5
|
+
* entity-encoded `&` → `&` (and `<`/`>`) on every string — corrupting
|
|
6
|
+
* URLs and any text containing those characters. XSS defense belongs at
|
|
7
|
+
* render time (template engines escape on output), not at storage. Storage
|
|
8
|
+
* stores literals, so this only trims.
|
|
9
|
+
*/
|
|
10
|
+
const chalk = require('chalk').default;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Recursively walk an object and trim all string leaf values.
|
|
14
|
+
* Returns a flat dot-notation update map of only the values that changed,
|
|
15
|
+
* plus an array of log entries describing each change.
|
|
16
|
+
*
|
|
17
|
+
* @param {*} value - The value to walk
|
|
18
|
+
* @param {string} prefix - Dot-notation path prefix
|
|
19
|
+
* @param {Object} result - Accumulator { updates, logs }
|
|
20
|
+
* @returns {Object} { updates, logs }
|
|
21
|
+
*/
|
|
22
|
+
function walkAndTrim(value, prefix, result) {
|
|
23
|
+
if (typeof value === 'string') {
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
if (trimmed !== value) {
|
|
26
|
+
result.updates[prefix] = trimmed;
|
|
27
|
+
|
|
28
|
+
const maxLen = 80;
|
|
29
|
+
const oldDisplay = value.length > maxLen ? `${value.slice(0, maxLen)}...` : value;
|
|
30
|
+
const newDisplay = trimmed.length > maxLen ? `${trimmed.slice(0, maxLen)}...` : trimmed;
|
|
31
|
+
result.logs.push(` ${chalk.dim(prefix)}: ${chalk.red(JSON.stringify(oldDisplay))} → ${chalk.green(JSON.stringify(newDisplay))}`);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
let arrayChanged = false;
|
|
38
|
+
const arrayLogs = [];
|
|
39
|
+
const trimmedArray = value.map((item, i) => {
|
|
40
|
+
if (typeof item === 'string') {
|
|
41
|
+
const trimmed = item.trim();
|
|
42
|
+
if (trimmed !== item) {
|
|
43
|
+
arrayChanged = true;
|
|
44
|
+
arrayLogs.push(` ${chalk.dim(`${prefix}[${i}]`)}: ${chalk.red(JSON.stringify(item))} → ${chalk.green(JSON.stringify(trimmed))}`);
|
|
45
|
+
}
|
|
46
|
+
return trimmed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Recurse into objects inside arrays
|
|
50
|
+
if (item !== null && typeof item === 'object') {
|
|
51
|
+
const nested = { updates: {}, logs: [] };
|
|
52
|
+
walkAndTrim(item, `${prefix}[${i}]`, nested);
|
|
53
|
+
if (Object.keys(nested.updates).length > 0) {
|
|
54
|
+
arrayChanged = true;
|
|
55
|
+
arrayLogs.push(...nested.logs);
|
|
56
|
+
|
|
57
|
+
// Apply nested updates to the item
|
|
58
|
+
for (const [path, val] of Object.entries(nested.updates)) {
|
|
59
|
+
const subPath = path.substring(`${prefix}[${i}].`.length);
|
|
60
|
+
const parts = subPath.split('.');
|
|
61
|
+
let target = item;
|
|
62
|
+
for (let j = 0; j < parts.length - 1; j++) {
|
|
63
|
+
target = target[parts[j]];
|
|
64
|
+
}
|
|
65
|
+
target[parts[parts.length - 1]] = val;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return item;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return item;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (arrayChanged && prefix) {
|
|
75
|
+
result.updates[prefix] = trimmedArray;
|
|
76
|
+
result.logs.push(...arrayLogs);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (value !== null && typeof value === 'object') {
|
|
82
|
+
for (const [key, child] of Object.entries(value)) {
|
|
83
|
+
const childPath = prefix ? `${prefix}.${key}` : key;
|
|
84
|
+
walkAndTrim(child, childPath, result);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a migration fix that recursively trims whitespace on all string
|
|
93
|
+
* values in a document. Attaches per-field before → after log lines via the
|
|
94
|
+
* `__logs__` sentinel so the migration runner prints them under the doc's
|
|
95
|
+
* `[N/total] <docId>` header instead of out-of-order.
|
|
96
|
+
*
|
|
97
|
+
* @returns {Function} Fix function for runMigration
|
|
98
|
+
*/
|
|
99
|
+
function createSanitizeFix() {
|
|
100
|
+
return (data) => {
|
|
101
|
+
const result = { updates: {}, logs: [] };
|
|
102
|
+
walkAndTrim(data, '', result);
|
|
103
|
+
|
|
104
|
+
if (Object.keys(result.updates).length === 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { ...result.updates, __logs__: result.logs };
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { createSanitizeFix };
|