@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,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .env cascade — the secrets mirror of the omega.json5 hierarchy (D15).
|
|
3
|
+
*
|
|
4
|
+
* Weakest → strongest: company .env ← brand .env ← local .env ← shell.
|
|
5
|
+
*
|
|
6
|
+
* Same walk as the config cascade (load.js owns it): a target inside a brand
|
|
7
|
+
* monorepo ({brand}/targets/{target}) layers the brand root's .env under its own,
|
|
8
|
+
* and a brand stamped with .omega/company.json layers its company root's
|
|
9
|
+
* .env underneath that. Loading uses dotenv's no-override semantics — keys
|
|
10
|
+
* already in process.env (the shell) always win, and files apply
|
|
11
|
+
* innermost-first, so local beats brand beats company.
|
|
12
|
+
*
|
|
13
|
+
* Every layer is TWO files, not one
|
|
14
|
+
* ([#586](https://github.com/Omega-JS-Stack/omega/issues/586)): its `.env` and
|
|
15
|
+
* the `.env.<environment>` overlay that wins over it — the widespread standard
|
|
16
|
+
* (Next.js, Vite, Rails dotenv, dotenv-flow). The environment names are exactly
|
|
17
|
+
* what envEnvironment() returns, so there is ONE vocabulary between the file
|
|
18
|
+
* name and the runtime's own answer, and only the RUNNING environment's overlay
|
|
19
|
+
* is ever read. Every key is equal: whatever the overlay holds wins, values are
|
|
20
|
+
* trusted, no key gets special treatment.
|
|
21
|
+
*
|
|
22
|
+
* Secrets are DEFINED once at their source level (a brand-wide GH_TOKEN in
|
|
23
|
+
* the brand .env, a company-wide key in the company .env) and RESOLVED here
|
|
24
|
+
* at runtime/build. Only a target that physically ships an env file still
|
|
25
|
+
* gets one materialized (dist/.env rides the Firebase deploy artifact —
|
|
26
|
+
* composeTargetEnv below builds it from this same chain, on every verb).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
|
|
32
|
+
const { findBrandRoot } = require('./load.js');
|
|
33
|
+
const { readCompanyRoot } = require('./company.js');
|
|
34
|
+
const { ENV_SCHEMA, envFileGroups } = require('./env-schema.js');
|
|
35
|
+
|
|
36
|
+
// The ONE environment vocabulary, strongest signal first: every `.env.<name>`
|
|
37
|
+
// overlay is suffixed with one of these, every framework's environment() answers
|
|
38
|
+
// one of these, and nothing anywhere spells a fourth
|
|
39
|
+
// ([#586](https://github.com/Omega-JS-Stack/omega/issues/586)).
|
|
40
|
+
const ENV_ENVIRONMENTS = ['development', 'testing', 'production'];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The runtime environment — the SINGLE SOURCE OF TRUTH for the one vocabulary,
|
|
44
|
+
* shared by the env overlay above and by every framework's own environment
|
|
45
|
+
* answer (@omega.js/backend's `env.environment()` / `Manager.getEnvironment()`
|
|
46
|
+
* delegate here). Exactly ONE of three mutually-exclusive values: testing wins,
|
|
47
|
+
* then production, else development.
|
|
48
|
+
*
|
|
49
|
+
* The final `else` is PRODUCTION on purpose: a deployed Cloud Function has no
|
|
50
|
+
* FUNCTIONS_EMULATOR and often no ENVIRONMENT var, so "no signal" IS the normal
|
|
51
|
+
* production state. Defaulting to development would make every deployed
|
|
52
|
+
* function skip real side effects (emails/analytics/webhooks). (Contrast
|
|
53
|
+
* UJM/BXM, whose deployed artifacts always carry their signal.)
|
|
54
|
+
*
|
|
55
|
+
* @returns {'testing'|'production'|'development'} The environment.
|
|
56
|
+
*/
|
|
57
|
+
function envEnvironment() {
|
|
58
|
+
// Testing takes precedence — set by the test runner / emulator (OMEGA_TEST_MODE=true).
|
|
59
|
+
if (process.env.OMEGA_TEST_MODE === 'true') {
|
|
60
|
+
return 'testing';
|
|
61
|
+
}
|
|
62
|
+
if (process.env.ENVIRONMENT === 'production') {
|
|
63
|
+
return 'production';
|
|
64
|
+
} else if (
|
|
65
|
+
process.env.ENVIRONMENT === 'development'
|
|
66
|
+
|| process.env.FUNCTIONS_EMULATOR === true
|
|
67
|
+
|| process.env.FUNCTIONS_EMULATOR === 'true'
|
|
68
|
+
|| process.env.TERM_PROGRAM === 'Apple_Terminal'
|
|
69
|
+
|| process.env.TERM_PROGRAM === 'vscode'
|
|
70
|
+
) {
|
|
71
|
+
return 'development';
|
|
72
|
+
} else {
|
|
73
|
+
return 'production';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The files ONE layer of the chain contributes, WEAKEST first: its `.env`, then
|
|
79
|
+
* the `.env.<environment>` overlay that wins over it (#586). A layer with no
|
|
80
|
+
* path contributes nothing; existence is not checked here.
|
|
81
|
+
*
|
|
82
|
+
* @param {string|null} envPath - The layer's base .env path.
|
|
83
|
+
* @param {string} [environment] - The running environment; absent = base only.
|
|
84
|
+
* @returns {string[]} Absolute paths, weakest first.
|
|
85
|
+
*/
|
|
86
|
+
function envLayerFiles(envPath, environment) {
|
|
87
|
+
if (!envPath) return [];
|
|
88
|
+
|
|
89
|
+
return environment ? [envPath, `${envPath}.${environment}`] : [envPath];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the .env chain for a project dir, strongest file first.
|
|
94
|
+
*
|
|
95
|
+
* `startDir` is the dir whose .env is the local layer — the project root for
|
|
96
|
+
* web/desktop/extension, the functions dir for a backend (its .env rides
|
|
97
|
+
* the deploy artifact). Brand discovery normalizes a target subdir (functions/,
|
|
98
|
+
* dist/) → target root, same as the config loader.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} startDir
|
|
101
|
+
* @returns {{ local: string, brand: string|null, company: string|null }}
|
|
102
|
+
* Absolute .env paths (existence not checked here).
|
|
103
|
+
*/
|
|
104
|
+
function resolveEnvChain(startDir) {
|
|
105
|
+
const targetDir = path.resolve(startDir);
|
|
106
|
+
const brandRoot = findBrandRoot(targetDir);
|
|
107
|
+
// The marker sits at the brand root; when startDir IS a brand root (no
|
|
108
|
+
// targets/ walk above it), its own marker supplies the company layer.
|
|
109
|
+
const companyRoot = readCompanyRoot(brandRoot || targetDir);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
local: path.join(targetDir, '.env'),
|
|
113
|
+
brand: brandRoot ? path.join(brandRoot, '.env') : null,
|
|
114
|
+
company: companyRoot ? path.join(companyRoot, '.env') : null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Key OWNERSHIP, remembered instead of inferred
|
|
119
|
+
// ([#724](https://github.com/Omega-JS-Stack/omega/issues/724)). The no-override
|
|
120
|
+
// rule below is what makes the shell win, but after a boot load EVERY key is
|
|
121
|
+
// "already in process.env", so presence alone can no longer tell a shell value
|
|
122
|
+
// from a file value — which is why a reload used to have to skip edits. Two
|
|
123
|
+
// sets keep the answer: what process.env carried BEFORE this process read its
|
|
124
|
+
// first file (shell-owned, forever), and what a file layer has put there since
|
|
125
|
+
// (file-owned, the only keys reloadEnv may drop and re-read).
|
|
126
|
+
let shellOwnedKeys = null;
|
|
127
|
+
const fileOwnedKeys = new Set();
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Record a key a file layer just delivered into process.env (#724).
|
|
131
|
+
*
|
|
132
|
+
* A key the shell brought stays shell-owned even if something deleted it and a
|
|
133
|
+
* file layer then supplied it — reloadEnv must never rewrite one.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} key
|
|
136
|
+
*/
|
|
137
|
+
function markFileOwned(key) {
|
|
138
|
+
if (!shellOwnedKeys.has(key)) fileOwnedKeys.add(key);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Load an ordered list of .env files (strongest first) with dotenv's
|
|
143
|
+
* no-override semantics: keys already in process.env (the shell, or a
|
|
144
|
+
* stronger file) always win, so load order = precedence. Null/missing
|
|
145
|
+
* entries skip silently.
|
|
146
|
+
*
|
|
147
|
+
* One rule on top of plain dotenv (dogfood friction #20): a file layer's
|
|
148
|
+
* EMPTY value (`KEY=` / `KEY=""`) never claims the key — empty means
|
|
149
|
+
* "documented here, value supplied by another layer", so a scaffolded
|
|
150
|
+
* local .env full of placeholders can't shadow the brand root's real
|
|
151
|
+
* values. Only the shell can deliberately set a key to empty.
|
|
152
|
+
*
|
|
153
|
+
* @param {Array<string|null>} envPaths
|
|
154
|
+
* @returns {string[]} The files that existed and were loaded.
|
|
155
|
+
*/
|
|
156
|
+
function loadEnvChain(envPaths) {
|
|
157
|
+
// The first chain load in the process fixes the shell-owned set: nothing here
|
|
158
|
+
// has read a file yet, so whatever process.env carries came from outside (#724)
|
|
159
|
+
if (shellOwnedKeys === null) shellOwnedKeys = new Set(Object.keys(process.env));
|
|
160
|
+
|
|
161
|
+
const loaded = [];
|
|
162
|
+
|
|
163
|
+
for (const envPath of envPaths) {
|
|
164
|
+
if (!envPath || !fs.existsSync(envPath)) continue;
|
|
165
|
+
|
|
166
|
+
const parsed = require('dotenv').parse(fs.readFileSync(envPath, 'utf8'));
|
|
167
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
168
|
+
if (value === '' || key in process.env) continue;
|
|
169
|
+
process.env[key] = value;
|
|
170
|
+
markFileOwned(key);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
loaded.push(envPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return loaded;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Load the .env cascade for a list of LAYER ROOTS, strongest root first: each
|
|
181
|
+
* root's `.env` plus the `.env.<environment>` overlay that wins over it (#586).
|
|
182
|
+
*
|
|
183
|
+
* The known-layers counterpart of loadEnv, which starts from a target dir and
|
|
184
|
+
* DISCOVERS its chain. The manager's walks already know theirs — the brand
|
|
185
|
+
* root, then the company root under it — and only need them loaded in order;
|
|
186
|
+
* a null root (a standalone brand's missing company layer) skips.
|
|
187
|
+
*
|
|
188
|
+
* @param {Array<string|null>} roots - Layer roots, strongest first.
|
|
189
|
+
* @param {object} [options]
|
|
190
|
+
* @param {string} [options.environment] - The environment whose overlay applies
|
|
191
|
+
* (defaults to the running one).
|
|
192
|
+
* @returns {string[]} The files that existed and were loaded.
|
|
193
|
+
*/
|
|
194
|
+
function loadEnvRoots(roots, { environment = envEnvironment() } = {}) {
|
|
195
|
+
// Strongest first, so a layer's overlay is offered before its own base
|
|
196
|
+
return loadEnvChain(roots.flatMap((root) => envLayerFiles(root && path.join(root, '.env'), environment).reverse()));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Apply the schema's `deliverAs` renames to a value map, in place — the ONE
|
|
201
|
+
* place the rename lives ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)).
|
|
202
|
+
*
|
|
203
|
+
* Per entry that carries `deliverAs` and names `target`: the brand-level name
|
|
204
|
+
* (`GOOGLE_ANALYTICS_SECRET_WEB`) becomes the runtime name
|
|
205
|
+
* (`GOOGLE_ANALYTICS_SECRET`) — a target only ever sees the one it reads, so
|
|
206
|
+
* the source name is consumed. A value already sitting under the delivered
|
|
207
|
+
* name WINS (the shell, or a human's explicit answer); the source name is
|
|
208
|
+
* consumed either way.
|
|
209
|
+
*
|
|
210
|
+
* Backend rides it through composeTargetEnv's dist/.env; every other target
|
|
211
|
+
* rides it through loadEnv, which delivers into process.env at CLI boot.
|
|
212
|
+
*
|
|
213
|
+
* @param {Object<string, string>} values - Mutated in place (a parsed layer, or process.env).
|
|
214
|
+
* @param {string} target - Target name ('backend', 'web', …).
|
|
215
|
+
* @returns {string[]} The delivered names that were set.
|
|
216
|
+
*/
|
|
217
|
+
function applyDeliverAs(values, target) {
|
|
218
|
+
const delivered = [];
|
|
219
|
+
|
|
220
|
+
for (const entry of ENV_SCHEMA) {
|
|
221
|
+
if (!entry.deliverAs || !entry.targets.includes(target)) continue;
|
|
222
|
+
|
|
223
|
+
const value = values[entry.name];
|
|
224
|
+
delete values[entry.name];
|
|
225
|
+
if (!value) continue;
|
|
226
|
+
if (values[entry.deliverAs]) continue;
|
|
227
|
+
|
|
228
|
+
values[entry.deliverAs] = value;
|
|
229
|
+
delivered.push(entry.deliverAs);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return delivered;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Resolve + load the full .env cascade for a project dir:
|
|
237
|
+
* shell > local .env > brand .env > company .env, each layer's
|
|
238
|
+
* `.env.<environment>` overlay winning over its own `.env` (#586).
|
|
239
|
+
*
|
|
240
|
+
* Pass the caller's `target` and the schema's `deliverAs` renames land in
|
|
241
|
+
* process.env too — the web/desktop/extension half of the delivery the
|
|
242
|
+
* backend gets from composeTargetEnv's dist/.env. Without a target nothing is
|
|
243
|
+
* renamed.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} startDir - See resolveEnvChain.
|
|
246
|
+
* @param {object} [options]
|
|
247
|
+
* @param {string} [options.target] - Target name ('web', 'desktop', …).
|
|
248
|
+
* @param {string} [options.environment] - The environment whose overlay applies
|
|
249
|
+
* (defaults to the running one).
|
|
250
|
+
* @returns {{ chain: { local: string, brand: string|null, company: string|null }, loaded: string[] }}
|
|
251
|
+
*/
|
|
252
|
+
function loadEnv(startDir, { target, environment = envEnvironment() } = {}) {
|
|
253
|
+
const chain = resolveEnvChain(startDir);
|
|
254
|
+
// Strongest first, so a layer's overlay is offered before its own base
|
|
255
|
+
const files = ['local', 'brand', 'company']
|
|
256
|
+
.flatMap((layer) => envLayerFiles(chain[layer], environment).reverse());
|
|
257
|
+
const loaded = loadEnvChain(files);
|
|
258
|
+
// A delivered name INHERITS its source key's ownership (#724): it carries a
|
|
259
|
+
// file layer's value under a second key, so it is normally file-owned too —
|
|
260
|
+
// otherwise an edited source value could not reach it. But the rename CONSUMES
|
|
261
|
+
// the source key, so once the shell exported that source name, the delivered
|
|
262
|
+
// name is the only place the shell's value still lives; calling it file-owned
|
|
263
|
+
// would let a reload drop it (gone when no file declares the source, replaced
|
|
264
|
+
// by the file's value when one does).
|
|
265
|
+
if (target) {
|
|
266
|
+
const shellSourced = new Set(ENV_SCHEMA
|
|
267
|
+
.filter((entry) => entry.deliverAs && entry.targets.includes(target) && shellOwnedKeys.has(entry.name))
|
|
268
|
+
.map((entry) => entry.deliverAs));
|
|
269
|
+
|
|
270
|
+
for (const key of applyDeliverAs(process.env, target)) {
|
|
271
|
+
// Inheritance is the whole answer, so a shell-sourced delivery also CLEARS
|
|
272
|
+
// a file-owned mark an earlier load left on that name
|
|
273
|
+
if (shellSourced.has(key)) fileOwnedKeys.delete(key);
|
|
274
|
+
else markFileOwned(key);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { chain, loaded };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Re-read the .env cascade for a project dir so EDITED file values land
|
|
282
|
+
* ([#724](https://github.com/Omega-JS-Stack/omega/issues/724)) — the reload
|
|
283
|
+
* half of loadEnv, same arguments, same answer.
|
|
284
|
+
*
|
|
285
|
+
* loadEnv alone cannot honor an edit: its no-override rule sees the key the
|
|
286
|
+
* boot load put there and skips it. So this DROPS every file-owned key first —
|
|
287
|
+
* the ones a file layer delivered, never one the shell brought — and then runs
|
|
288
|
+
* the same load. Consequences, all of them the file being re-read rather than
|
|
289
|
+
* merged onto the old set:
|
|
290
|
+
* - an EDITED value lands, and so does a NEW key;
|
|
291
|
+
* - a key DROPPED from the file is dropped from the process;
|
|
292
|
+
* - a SHELL-set key is untouched, whatever any file now says.
|
|
293
|
+
*
|
|
294
|
+
* It re-reads the chain it is GIVEN, so a process that loaded several projects'
|
|
295
|
+
* cascades keeps only the reloaded one's file keys. The dev lanes' `.env`
|
|
296
|
+
* watchers (#681) are the caller, and each watches its own single target.
|
|
297
|
+
*
|
|
298
|
+
* @param {string} startDir - See resolveEnvChain.
|
|
299
|
+
* @param {object} [options] - See loadEnv.
|
|
300
|
+
* @param {string} [options.target] - Target name ('web', 'desktop', …).
|
|
301
|
+
* @param {string} [options.environment] - The environment whose overlay applies.
|
|
302
|
+
* @returns {{ chain: { local: string, brand: string|null, company: string|null }, loaded: string[] }}
|
|
303
|
+
*/
|
|
304
|
+
function reloadEnv(startDir, { target, environment = envEnvironment() } = {}) {
|
|
305
|
+
for (const key of fileOwnedKeys) delete process.env[key];
|
|
306
|
+
fileOwnedKeys.clear();
|
|
307
|
+
|
|
308
|
+
return loadEnv(startDir, { target, environment });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Parse one .env file into a plain map. Missing files read as empty.
|
|
313
|
+
*
|
|
314
|
+
* @param {string|null} envPath
|
|
315
|
+
* @returns {Object<string, string>} Parsed key → value.
|
|
316
|
+
*/
|
|
317
|
+
function parseEnvFile(envPath) {
|
|
318
|
+
if (!envPath || !fs.existsSync(envPath)) return {};
|
|
319
|
+
|
|
320
|
+
return require('dotenv').parse(fs.readFileSync(envPath, 'utf8'));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The schema entry that DELIVERS `key` to `target`, if any — an exact name
|
|
325
|
+
* first, then the dynamic families' patterns (`envSchemaEntry`'s order). A key
|
|
326
|
+
* is delivered only when its entry names the target AND its group renders into
|
|
327
|
+
* a real file: the `file: false` groups are the backend's own resolution lanes
|
|
328
|
+
* (config at boot, the developer's shell), never something a brand hands down.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} key - The env var name in the brand/company layer.
|
|
331
|
+
* @param {string} target - Target name ('backend', 'web', …).
|
|
332
|
+
* @returns {object|undefined} The entry, or undefined when the key stays home.
|
|
333
|
+
*/
|
|
334
|
+
function deliveringEntry(key, target) {
|
|
335
|
+
const fileGroups = new Set(envFileGroups().map((group) => group.id));
|
|
336
|
+
const claims = (entry) => entry.targets.includes(target) && fileGroups.has(entry.group);
|
|
337
|
+
|
|
338
|
+
return ENV_SCHEMA.find((entry) => entry.name === key && claims(entry))
|
|
339
|
+
|| ENV_SCHEMA.find((entry) => entry.match instanceof RegExp && entry.match.test(key) && claims(entry));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Compose the env a target's own artifact ships with
|
|
344
|
+
* ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)).
|
|
345
|
+
*
|
|
346
|
+
* FILES ONLY, never the shell: company .env ← brand .env ← target .env, the
|
|
347
|
+
* same chain resolveEnvChain walks, strongest last. The brand root's .env is
|
|
348
|
+
* the ONE file humans and the manager edit; a target's own .env is an optional
|
|
349
|
+
* per-key override a human writes. Nothing here reads or writes process.env —
|
|
350
|
+
* a build must produce the same artifact under any shell.
|
|
351
|
+
*
|
|
352
|
+
* Each layer is its `.env` plus the `.env.<environment>` overlay that wins over
|
|
353
|
+
* it (#586), so ONE flat artifact ships for ONE environment: a deploy composes
|
|
354
|
+
* base + production, the emulator base + development, a test lane base + testing
|
|
355
|
+
* — and no other environment's file ever rides along.
|
|
356
|
+
*
|
|
357
|
+
* The two brand-side layers are FILTERED by the env schema (the only filter
|
|
358
|
+
* there is): a key rides down when some entry claims it — by name or by
|
|
359
|
+
* pattern — names this target, and sits in a file group. The TARGET layer
|
|
360
|
+
* passes through unfiltered: placing a key in the target's own .env IS the
|
|
361
|
+
* targeting. `deliverAs` renames on arrival in EVERY layer (the per-target GA4
|
|
362
|
+
* secrets), so a human writing the brand-level name in the target's own .env
|
|
363
|
+
* gets the one delivered key, overriding the brand's.
|
|
364
|
+
*
|
|
365
|
+
* An empty value never claims a key, the same rule loadEnvChain applies: empty
|
|
366
|
+
* means "documented here, valued by another layer".
|
|
367
|
+
*
|
|
368
|
+
* @param {object} options
|
|
369
|
+
* @param {string} options.targetDir - The target root (its .env is the local layer).
|
|
370
|
+
* @param {string} options.target - Target name ('backend', 'web', …).
|
|
371
|
+
* @param {string} [options.environment] - The environment whose overlay composes
|
|
372
|
+
* (defaults to the running one).
|
|
373
|
+
* @returns {{ values: Object<string, string>, sources: Object<string, string> }}
|
|
374
|
+
* `sources` maps each delivered key to the layer it came from
|
|
375
|
+
* (`company`/`brand`/`target`) — an overlay reports as its own layer, for
|
|
376
|
+
* logging by key NAME only.
|
|
377
|
+
*/
|
|
378
|
+
function composeTargetEnv({ targetDir, target, environment = envEnvironment() }) {
|
|
379
|
+
const chain = resolveEnvChain(targetDir);
|
|
380
|
+
const values = {};
|
|
381
|
+
const sources = {};
|
|
382
|
+
|
|
383
|
+
const deliver = (key, value, layer) => {
|
|
384
|
+
if (!value) return;
|
|
385
|
+
values[key] = value;
|
|
386
|
+
sources[key] = layer;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Weakest first — each file overwrites what the ones below it delivered, and
|
|
390
|
+
// a layer's overlay sits directly above its own base
|
|
391
|
+
const files = [
|
|
392
|
+
...envLayerFiles(chain.company, environment).map((file) => ({ file, layer: 'company', filtered: true })),
|
|
393
|
+
...envLayerFiles(chain.brand, environment).map((file) => ({ file, layer: 'brand', filtered: true })),
|
|
394
|
+
// The TARGET layer passes through unfiltered: placement IS the targeting
|
|
395
|
+
...envLayerFiles(chain.local, environment).map((file) => ({ file, layer: 'target', filtered: false })),
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
for (const { file, layer, filtered } of files) {
|
|
399
|
+
const claimed = {};
|
|
400
|
+
for (const [key, value] of Object.entries(parseEnvFile(file))) {
|
|
401
|
+
if (filtered && !deliveringEntry(key, target)) continue;
|
|
402
|
+
claimed[key] = value;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
applyDeliverAs(claimed, target);
|
|
406
|
+
for (const [key, value] of Object.entries(claimed)) {
|
|
407
|
+
deliver(key, value, layer);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { values, sources };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Serialize one value as a double-quoted .env line — the serializer SSOT every
|
|
416
|
+
* writeback rides (this composer, the manager's brand .env writeback and
|
|
417
|
+
* scaffold stub). Backslashes, quotes and newlines escape so a multi-line blob
|
|
418
|
+
* stays line-safe (dotenv expands `\n` back on read).
|
|
419
|
+
*
|
|
420
|
+
* @param {string} key - Env var name.
|
|
421
|
+
* @param {string} value - Value to serialize.
|
|
422
|
+
* @returns {string} `KEY="value"`.
|
|
423
|
+
*/
|
|
424
|
+
function envLine(key, value) {
|
|
425
|
+
const escaped = String(value)
|
|
426
|
+
.replace(/\\/g, '\\\\')
|
|
427
|
+
.replace(/"/g, '\\"')
|
|
428
|
+
.replace(/\n/g, '\\n');
|
|
429
|
+
|
|
430
|
+
return `${key}="${escaped}"`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Serialize composed values as .env content — one envLine per key.
|
|
435
|
+
*
|
|
436
|
+
* @param {Object<string, string>} values
|
|
437
|
+
* @returns {string} The file content, newline-terminated.
|
|
438
|
+
*/
|
|
439
|
+
function serializeEnv(values) {
|
|
440
|
+
const lines = Object.entries(values).map(([key, value]) => envLine(key, value));
|
|
441
|
+
|
|
442
|
+
return `${lines.join('\n')}\n`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
module.exports = { loadEnv, reloadEnv, ENV_ENVIRONMENTS, envEnvironment, resolveEnvChain, envLayerFiles, loadEnvChain, loadEnvRoots, applyDeliverAs, composeTargetEnv, envLine, serializeEnv };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company/brand hooks — owner-supplied code the frameworks call at named
|
|
3
|
+
* hook points, so company-specific logic (e.g. a password formula) lives in
|
|
4
|
+
* the OWNER'S tree, never in framework source.
|
|
5
|
+
*
|
|
6
|
+
* Layout is NESTED, mirroring the call site that invokes the hook (decided
|
|
7
|
+
* with Ian 2026-07-10): the account service's password step loads
|
|
8
|
+
* `config/hooks/account/password.js`, a future onboarding hook would live
|
|
9
|
+
* under `config/hooks/onboard/…` — one file per hook point, path = the
|
|
10
|
+
* invoking structure, never a flat name-mangled file.
|
|
11
|
+
*
|
|
12
|
+
* Hooks live under `config/` — the established home for owner-authored
|
|
13
|
+
* omega inputs (omega.json5, seo.json5, chatsy.md, …) — and are therefore
|
|
14
|
+
* VERSIONED by default (Ian 2026-07-11: hooks are authored code, and a
|
|
15
|
+
* gitignored hook lost on a fresh clone would silently change behavior —
|
|
16
|
+
* e.g. account passwords falling back to the seed channel). Never `.omega/`,
|
|
17
|
+
* which is machine-owned bookkeeping. Secrets still belong in .env — a hook
|
|
18
|
+
* that needs one reads process.env; owners who truly want a hook out of git
|
|
19
|
+
* add their own `config/hooks/` ignore line.
|
|
20
|
+
*
|
|
21
|
+
* Resolution walks the same hierarchy as the .env cascade: the brand root's
|
|
22
|
+
* own `config/hooks/` first, then the company root's (via the
|
|
23
|
+
* .omega/company.json stamp) — so a company-wide hook covers every brand and
|
|
24
|
+
* a single brand can still override it. Hooks are plain CJS modules whose
|
|
25
|
+
* `module.exports` IS the hook function; call-site docs define each hook's
|
|
26
|
+
* signature and return contract.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
|
|
32
|
+
const { readCompanyRoot } = require('./company.js');
|
|
33
|
+
|
|
34
|
+
// Hook points are code-owned kebab-case path constants ('account/password') —
|
|
35
|
+
// enforce the shape so a typo'd or traversal-shaped path fails loudly.
|
|
36
|
+
const HOOK_PATH_PATTERN = /^[a-z0-9-]+(\/[a-z0-9-]+)*$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The candidate file for one hook point under one root.
|
|
40
|
+
*/
|
|
41
|
+
function hookFile(root, hookPath) {
|
|
42
|
+
return `${path.join(root, 'config', 'hooks', ...hookPath.split('/'))}.js`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a hook point to the file that defines it: the brand root's own
|
|
47
|
+
* `config/hooks/<hookPath>.js`, else the company root's (company.json stamp).
|
|
48
|
+
*
|
|
49
|
+
* @param {string} startRoot - Brand (or standalone-project) root
|
|
50
|
+
* @param {string} hookPath - Call-site-mirroring hook point, e.g. 'account/password'
|
|
51
|
+
* @returns {string|null} Absolute hook file path, or null when neither root defines it
|
|
52
|
+
*/
|
|
53
|
+
function resolveHook(startRoot, hookPath) {
|
|
54
|
+
if (!HOOK_PATH_PATTERN.test(hookPath)) {
|
|
55
|
+
throw new Error(`Invalid hook path ${JSON.stringify(hookPath)} — kebab-case segments joined by '/', e.g. 'account/password'`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const roots = [path.resolve(startRoot)];
|
|
59
|
+
const companyRoot = readCompanyRoot(roots[0]);
|
|
60
|
+
if (companyRoot && companyRoot !== roots[0]) {
|
|
61
|
+
roots.push(companyRoot);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return roots.map((root) => hookFile(root, hookPath)).find((file) => fs.existsSync(file)) || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Load a hook point's function. Absent hooks return null (callers fall
|
|
69
|
+
* through to their default behavior); a hook file that exists but is broken
|
|
70
|
+
* — unloadable, or not exporting a function — throws, because an owner who
|
|
71
|
+
* wrote a hook must never get silent fallback.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} startRoot - Brand (or standalone-project) root
|
|
74
|
+
* @param {string} hookPath - Call-site-mirroring hook point, e.g. 'account/password'
|
|
75
|
+
* @returns {{ fn: Function, file: string }|null}
|
|
76
|
+
*/
|
|
77
|
+
function loadHook(startRoot, hookPath) {
|
|
78
|
+
const file = resolveHook(startRoot, hookPath);
|
|
79
|
+
if (!file) return null;
|
|
80
|
+
|
|
81
|
+
let exported;
|
|
82
|
+
try {
|
|
83
|
+
exported = require(file);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
throw new Error(`Hook ${hookPath} failed to load (${file}): ${e.message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// CJS module.exports = fn is the contract; tolerate an ESM default export
|
|
89
|
+
const fn = typeof exported === 'function' ? exported : exported?.default;
|
|
90
|
+
if (typeof fn !== 'function') {
|
|
91
|
+
throw new Error(`Hook ${hookPath} (${file}) must export a function (module.exports = ({ … }) => …)`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { fn, file };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { resolveHook, loadHook };
|