@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,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devlog — auto-generated commit-digest blog posts: collect recent commits
|
|
3
|
+
* across GitHub orgs + brand repos → have Ghostii write a first-person devlog
|
|
4
|
+
* article → publish to the brand's website target (syndication platforms later).
|
|
5
|
+
*
|
|
6
|
+
* Standalone command, not a service: publishing a new post every run is not
|
|
7
|
+
* idempotent reconciliation, so devlog never runs during manage. Stateless by
|
|
8
|
+
* design — every run computes `since = now − lookbackDays` and fetches fresh;
|
|
9
|
+
* nothing is read from or written to .omega/runs/. Running
|
|
10
|
+
* twice in one window writes two posts about the same commits — the cadence
|
|
11
|
+
* is owned by whoever runs it.
|
|
12
|
+
*
|
|
13
|
+
* Works from a brand root (that brand publishes; company siblings via the
|
|
14
|
+
* .omega/company.json stamp still feed the backlink map) or a company root
|
|
15
|
+
* (--brand, or the single brand with devlog.enabled).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { join } = require('node:path');
|
|
19
|
+
|
|
20
|
+
const chalk = require('chalk').default;
|
|
21
|
+
const jetpack = require('fs-jetpack');
|
|
22
|
+
const { loadEnvRoots, chosenProvider } = require('../vendor/config/index.js');
|
|
23
|
+
|
|
24
|
+
const { resolveBrandRoot, loadBrand } = require('../lib/brand.js');
|
|
25
|
+
const {
|
|
26
|
+
isCompanyRoot, discoverBrands, readCompanyMarker,
|
|
27
|
+
} = require('../lib/company.js');
|
|
28
|
+
const { GitHubAPI } = require('../services/repo/lib/github-api.js');
|
|
29
|
+
const { collectCommits } = require('./lib/collect-commits.js');
|
|
30
|
+
const { buildProjectMap, buildBrandRepos } = require('./lib/project-map.js');
|
|
31
|
+
const { generatePost } = require('./lib/generate-post.js');
|
|
32
|
+
const { publishToWebsite, renderPostFile } = require('./lib/publish-website.js');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pick the brand that publishes: explicit --brand wins; otherwise exactly one
|
|
36
|
+
* loaded brand must have devlog.enabled.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array} brands - Loaded brands (lib/brand.js loadBrand shape)
|
|
39
|
+
* @param {string} [explicitId] - Brand ID passed via --brand
|
|
40
|
+
* @returns {object} The publishing brand
|
|
41
|
+
*/
|
|
42
|
+
function pickBrand(brands, explicitId) {
|
|
43
|
+
if (explicitId) {
|
|
44
|
+
const brand = brands.find((b) => b.id === explicitId);
|
|
45
|
+
if (!brand) {
|
|
46
|
+
throw new Error(`Brand not found: ${explicitId} (known: ${brands.map((b) => b.id).join(', ')})`);
|
|
47
|
+
}
|
|
48
|
+
return brand;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const enabled = brands.filter((b) => b.config.devlog.enabled === true);
|
|
52
|
+
|
|
53
|
+
if (enabled.length === 0) {
|
|
54
|
+
throw new Error('No brand has devlog.enabled — set it in the brand\'s config/omega.json5 or pass --brand');
|
|
55
|
+
}
|
|
56
|
+
if (enabled.length > 1) {
|
|
57
|
+
throw new Error(`Multiple brands have devlog.enabled (${enabled.map((b) => b.id).join(', ')}) — pass --brand`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return enabled[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the run context from any cwd: the publishing brand, every sibling
|
|
65
|
+
* brand (they feed the backlink map — the brand configs are the SSOT for
|
|
66
|
+
* where repos live), and the company root when one is in play.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} startDir - Any directory inside a brand or company workspace
|
|
69
|
+
* @param {string} [explicitId] - Brand ID passed via --brand
|
|
70
|
+
* @returns {{ brand: object, brands: Array, companyRoot: string|null }}
|
|
71
|
+
*/
|
|
72
|
+
function resolveContext(startDir, explicitId) {
|
|
73
|
+
const root = resolveBrandRoot(startDir);
|
|
74
|
+
|
|
75
|
+
if (!root) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`No brand or company workspace found at or above ${startDir} — `
|
|
78
|
+
+ 'expected a config/omega.json5 at the root (see docs/shared/config.md).',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Company root: every managed brand loads with the company layer (folded by
|
|
83
|
+
// @omega.js/config off each brand's own .omega/company.json stamp — #83); the
|
|
84
|
+
// publishing brand is picked from among them.
|
|
85
|
+
if (isCompanyRoot(root)) {
|
|
86
|
+
const brands = discoverBrands(root).brands.map((b) => loadBrand(b.root));
|
|
87
|
+
|
|
88
|
+
if (brands.length === 0) {
|
|
89
|
+
throw new Error(`No brands found under ${root} — nothing to devlog about`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { brand: pickBrand(brands, explicitId), brands, companyRoot: root };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Brand root: this brand publishes. The company stamp (when present and
|
|
96
|
+
// fresh) surfaces the sibling brands for the project map and the company
|
|
97
|
+
// .env; the config layer itself rides the same stamp inside loadBrand.
|
|
98
|
+
const marker = readCompanyMarker(root);
|
|
99
|
+
let companyRoot = null;
|
|
100
|
+
|
|
101
|
+
if (marker?.stale) {
|
|
102
|
+
console.log(chalk.yellow(`⚠ .omega/company.json points at ${marker.companyRoot}, which is no longer a company workspace — running standalone`));
|
|
103
|
+
} else if (marker) {
|
|
104
|
+
companyRoot = marker.companyRoot;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const brand = loadBrand(root);
|
|
108
|
+
|
|
109
|
+
if (explicitId && explicitId !== brand.id) {
|
|
110
|
+
throw new Error(`--brand=${explicitId} does not match this brand root (${brand.id}) — run from the company root to target siblings`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let brands = [brand];
|
|
114
|
+
if (companyRoot) {
|
|
115
|
+
const siblings = discoverBrands(companyRoot).brands
|
|
116
|
+
.filter((b) => b.id !== brand.id)
|
|
117
|
+
.map((b) => loadBrand(b.root));
|
|
118
|
+
brands = [brand, ...siblings];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { brand, brands, companyRoot };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Main devlog runner: collect commits → map projects → generate post →
|
|
126
|
+
* publish (or preview on --dry-run).
|
|
127
|
+
*
|
|
128
|
+
* @param {string} startDir - Any directory inside the brand/company workspace
|
|
129
|
+
* @param {object} [options] - { brand?, days?, dryRun? }
|
|
130
|
+
* @param {object} [deps] - Test seams: { githubApi?, generatePost? } — prod
|
|
131
|
+
* callers pass nothing and get the real GitHubAPI + Ghostii pipeline
|
|
132
|
+
* @returns {Promise<{ published: boolean, commits: number, previewPath?: string, posts?: Array }>}
|
|
133
|
+
*/
|
|
134
|
+
async function runDevlog(startDir, options = {}, deps = {}) {
|
|
135
|
+
console.log(chalk.bold.cyan('Omega Manager — Devlog'));
|
|
136
|
+
console.log('');
|
|
137
|
+
|
|
138
|
+
const { brand, brands, companyRoot } = resolveContext(startDir, options.brand);
|
|
139
|
+
|
|
140
|
+
if (!brand.config.devlog.enabled) {
|
|
141
|
+
throw new Error(`Devlog is disabled for ${brand.id} — set devlog.enabled: true`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The writer is a KEY under devlog.providers (#425) and its settings live
|
|
145
|
+
// inside it — the role level carries only `enabled`.
|
|
146
|
+
const writer = chosenProvider(brand.config.devlog.providers);
|
|
147
|
+
if (!writer) {
|
|
148
|
+
throw new Error(`No devlog writer configured for ${brand.id} — add devlog.providers.ghostii`);
|
|
149
|
+
}
|
|
150
|
+
const settings = brand.config.devlog.providers[writer];
|
|
151
|
+
|
|
152
|
+
if (!settings.orgs.length) {
|
|
153
|
+
throw new Error(`No devlog.providers.${writer}.orgs configured for ${brand.id}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Secrets chain: shell env > brand .env > company .env — the shared
|
|
157
|
+
// cascade (files load strongest-first, never overriding what's set), each
|
|
158
|
+
// layer overlaid by its own `.env.<environment>` file (#586). `production`
|
|
159
|
+
// is PINNED, never the shell's answer: a devlog run reads REAL GitHub and
|
|
160
|
+
// publishes with the brand's real writer account.
|
|
161
|
+
loadEnvRoots([brand.root, companyRoot], { environment: 'production' });
|
|
162
|
+
|
|
163
|
+
const days = Number(options.days || settings.lookbackDays);
|
|
164
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
165
|
+
|
|
166
|
+
console.log(`${chalk.dim('→')} Brand: ${chalk.cyan(brand.config.brand?.name || brand.id)} (${brand.id})`);
|
|
167
|
+
console.log(`${chalk.dim('→')} Window: last ${days} days (since ${since.toISOString()})`);
|
|
168
|
+
console.log('');
|
|
169
|
+
|
|
170
|
+
// 1. Brand configs are the SSOT for repos: the backlink map + the full repo
|
|
171
|
+
// list across every brand (the `orgs` list only adds non-brand repos
|
|
172
|
+
// like frameworks and tooling)
|
|
173
|
+
const projectMap = buildProjectMap(brands, { excludeRepos: settings.excludeRepos });
|
|
174
|
+
const brandRepos = buildBrandRepos(brands, { excludeRepos: settings.excludeRepos });
|
|
175
|
+
|
|
176
|
+
// 2. Collect commits from GitHub
|
|
177
|
+
const api = deps.githubApi || new GitHubAPI();
|
|
178
|
+
const { commits, repos } = collectCommits({
|
|
179
|
+
api,
|
|
180
|
+
orgs: settings.orgs,
|
|
181
|
+
brandRepos,
|
|
182
|
+
since,
|
|
183
|
+
excludeRepos: settings.excludeRepos,
|
|
184
|
+
excludeCommits: settings.excludeCommits,
|
|
185
|
+
includePrivate: settings.includePrivate,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!commits.length) {
|
|
189
|
+
console.log(`${chalk.yellow('⚠')} No commits found in the last ${days} days — nothing to write about`);
|
|
190
|
+
return { published: false, commits: 0 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(`${chalk.green('✓')} Collected ${commits.length} commits across ${repos.length} repos`);
|
|
195
|
+
|
|
196
|
+
// 3. Generate the post
|
|
197
|
+
console.log(`${chalk.dim('→')} Generating post via ${writer}...`);
|
|
198
|
+
const generate = deps.generatePost || generatePost;
|
|
199
|
+
const post = await generate({ brandConfig: brand.config, commits, projectMap, days });
|
|
200
|
+
console.log(`${chalk.green('✓')} Generated: ${chalk.cyan(post.title)}`);
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
// 4. Publish (or preview on --dry-run)
|
|
204
|
+
if (options.dryRun) {
|
|
205
|
+
const previewPath = join(brand.root, '.omega', 'devlog', `${post.slug}.md`);
|
|
206
|
+
jetpack.write(previewPath, renderPostFile(brand.config, post));
|
|
207
|
+
console.log(`${chalk.green('✓')} Dry run — preview written to ${chalk.cyan(previewPath)}`);
|
|
208
|
+
return { published: false, commits: commits.length, previewPath };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const posts = [];
|
|
212
|
+
for (const destination of settings.destinations) {
|
|
213
|
+
if (destination === 'website') {
|
|
214
|
+
const { postPath, url } = publishToWebsite({ brand, post });
|
|
215
|
+
posts.push({ destination, postPath, url });
|
|
216
|
+
console.log(`${chalk.green('✓')} Website: ${chalk.cyan(postPath)}`);
|
|
217
|
+
console.log(` ${chalk.dim('→')} Live at ${chalk.cyan(url)} after the site build finishes`);
|
|
218
|
+
} else {
|
|
219
|
+
console.log(`${chalk.yellow('⚠')} Destination not implemented yet: ${destination}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { published: posts.length > 0, commits: commits.length, posts };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { runDevlog };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devlog collect stage — build the scan worklist (org listings + brand-derived
|
|
3
|
+
* repos) and pull every eligible commit since the window start via the gh CLI.
|
|
4
|
+
* Org listings come with metadata (archived, pushed_at, private, homepage) so
|
|
5
|
+
* inactive repos are pre-filtered; brand repos get a direct commits probe.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk').default;
|
|
9
|
+
|
|
10
|
+
// Newest-first cap per repo so fleet-wide automated pushes can't drown the digest
|
|
11
|
+
const MAX_COMMITS_PER_REPO = 30;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run a paginated `gh api` call and merge multi-page output.
|
|
15
|
+
* `--paginate` concatenates page arrays back-to-back (`[...][...]`), which
|
|
16
|
+
* breaks JSON.parse — join the boundaries before parsing.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} api - GitHubAPI client
|
|
19
|
+
* @param {string} path - API path including query string
|
|
20
|
+
* @returns {Array} Parsed results (empty array when no output)
|
|
21
|
+
*/
|
|
22
|
+
function runPaginatedJson(api, path) {
|
|
23
|
+
const raw = api.runCommand(['api', path, '--paginate']);
|
|
24
|
+
|
|
25
|
+
if (!raw) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return JSON.parse(raw.replace(/\]\s*\[/g, ','));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* List repos for a GitHub owner — org endpoint first, user endpoint fallback
|
|
34
|
+
* (devlog.providers.ghostii.orgs may mix orgs and personal accounts).
|
|
35
|
+
*
|
|
36
|
+
* @param {object} api - GitHubAPI client
|
|
37
|
+
* @param {string} owner - Org or user name
|
|
38
|
+
* @returns {Array} Repos
|
|
39
|
+
*/
|
|
40
|
+
function listReposForOwner(api, owner) {
|
|
41
|
+
try {
|
|
42
|
+
return runPaginatedJson(api, `orgs/${owner}/repos?per_page=100`);
|
|
43
|
+
} catch {
|
|
44
|
+
return runPaginatedJson(api, `users/${owner}/repos?per_page=100`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Collect commits since a given date across the full scan worklist: every
|
|
50
|
+
* repo in the configured `orgs` listings PLUS every brand repo derived from
|
|
51
|
+
* the brand configs. Skips archived/excluded repos, merge/bot/fleet commits,
|
|
52
|
+
* and excludeCommits matches.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} params - { api, orgs, brandRepos, since, excludeRepos, excludeCommits, includePrivate }
|
|
55
|
+
* @returns {{ commits: Array, repos: Array<string> }} Commits + active repo full names
|
|
56
|
+
*/
|
|
57
|
+
function collectCommits({ api, orgs = [], brandRepos = [], since, excludeRepos = [], excludeCommits = [], includePrivate = true }) {
|
|
58
|
+
const sinceIso = since.toISOString();
|
|
59
|
+
const excludePatterns = excludeCommits.map((pattern) => new RegExp(pattern, 'i'));
|
|
60
|
+
const commits = [];
|
|
61
|
+
const activeRepos = [];
|
|
62
|
+
|
|
63
|
+
// Build the worklist. Org listings come with metadata (pushed_at, private,
|
|
64
|
+
// homepage) so inactive repos are pre-filtered; brand-derived repos from
|
|
65
|
+
// other orgs have no listing, so they get a direct commits probe instead.
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const worklist = [];
|
|
68
|
+
|
|
69
|
+
for (const owner of orgs) {
|
|
70
|
+
console.log(`${chalk.dim('→')} Listing ${chalk.cyan(owner)}...`);
|
|
71
|
+
|
|
72
|
+
for (const repo of listReposForOwner(api, owner)) {
|
|
73
|
+
seen.add(`${owner}/${repo.name}`.toLowerCase());
|
|
74
|
+
|
|
75
|
+
const eligible = !repo.archived
|
|
76
|
+
&& new Date(repo.pushed_at) >= since
|
|
77
|
+
&& !excludeRepos.includes(repo.name)
|
|
78
|
+
&& (includePrivate || !repo.private);
|
|
79
|
+
|
|
80
|
+
if (eligible) {
|
|
81
|
+
worklist.push({ owner, repo: repo.name, homepage: repo.homepage || '' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const { owner, repo } of brandRepos) {
|
|
87
|
+
if (!seen.has(`${owner}/${repo}`.toLowerCase())) {
|
|
88
|
+
worklist.push({ owner, repo, homepage: '' });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`${chalk.dim('→')} Scanning ${worklist.length} repos...`);
|
|
93
|
+
|
|
94
|
+
worklist.forEach(({ owner, repo, homepage }, index) => {
|
|
95
|
+
let raw;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
raw = runPaginatedJson(api, `repos/${owner}/${repo}/commits?since=${sinceIso}&per_page=100`);
|
|
99
|
+
} catch {
|
|
100
|
+
// Brand-derived repos may not exist yet (404) or be empty (409) — skip quietly
|
|
101
|
+
console.log(` ${chalk.dim(`[${index + 1}/${worklist.length}]`)} ${owner}/${repo}: ${chalk.dim('unavailable (missing or empty)')}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const cleaned = raw
|
|
106
|
+
.filter((c) => !c.commit.message.startsWith('Merge '))
|
|
107
|
+
.filter((c) => !c.commit.message.startsWith('📦 Omega:')) // fleet-automation commits — noise, and automated content is banned anyway
|
|
108
|
+
.filter((c) => !excludePatterns.some((pattern) => pattern.test(c.commit.message))) // banned-topic commits never reach the writer
|
|
109
|
+
.filter((c) => !(c.author?.login || '').endsWith('[bot]') && !(c.commit.author?.name || '').endsWith('[bot]'))
|
|
110
|
+
.map((c) => ({
|
|
111
|
+
owner,
|
|
112
|
+
repo,
|
|
113
|
+
homepage,
|
|
114
|
+
sha: c.sha.slice(0, 7),
|
|
115
|
+
message: c.commit.message.split('\n')[0],
|
|
116
|
+
date: c.commit.author?.date,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
const trimmed = cleaned.length > MAX_COMMITS_PER_REPO
|
|
120
|
+
? chalk.dim(` (keeping latest ${MAX_COMMITS_PER_REPO})`)
|
|
121
|
+
: '';
|
|
122
|
+
console.log(` ${chalk.dim(`[${index + 1}/${worklist.length}]`)} ${owner}/${repo}: ${cleaned.length} commits${trimmed}`);
|
|
123
|
+
|
|
124
|
+
// API returns newest first, so slice keeps the most recent
|
|
125
|
+
const kept = cleaned.slice(0, MAX_COMMITS_PER_REPO);
|
|
126
|
+
|
|
127
|
+
if (kept.length) {
|
|
128
|
+
commits.push(...kept);
|
|
129
|
+
activeRepos.push(`${owner}/${repo}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return { commits, repos: activeRepos };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { collectCommits };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devlog generate stage — group the collected commits into a per-repo digest
|
|
3
|
+
* (Ghostii's sourceContent), assemble the brief (description), and have the
|
|
4
|
+
* provider write the article. Project URLs go through Ghostii's `links`
|
|
5
|
+
* param — its allocator assigns them to sections natively.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk').default;
|
|
9
|
+
const { chosenProvider } = require('../../vendor/config/index.js');
|
|
10
|
+
|
|
11
|
+
const { writeArticle, blocksToPost, MAX_DESCRIPTION_LENGTH, MAX_SOURCE_CONTENT_LENGTH } = require('./ghostii.js');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Group commits by repo and render the digest Ghostii works from (sourceContent).
|
|
15
|
+
* Each repo header carries its project mapping so the writer knows which
|
|
16
|
+
* product the work belongs to.
|
|
17
|
+
*
|
|
18
|
+
* @param {Array} commits - Collected commits
|
|
19
|
+
* @param {object} projectMap - repoName → { project, url }
|
|
20
|
+
* @returns {string} Digest text
|
|
21
|
+
*/
|
|
22
|
+
function buildDigest(commits, projectMap) {
|
|
23
|
+
const byRepo = new Map();
|
|
24
|
+
|
|
25
|
+
for (const commit of commits) {
|
|
26
|
+
const key = `${commit.owner}/${commit.repo}`;
|
|
27
|
+
|
|
28
|
+
if (!byRepo.has(key)) {
|
|
29
|
+
byRepo.set(key, []);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
byRepo.get(key).push(commit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sections = [];
|
|
36
|
+
|
|
37
|
+
for (const [fullName, repoCommits] of byRepo) {
|
|
38
|
+
const [, repo] = fullName.split('/');
|
|
39
|
+
const mapped = projectMap[repo];
|
|
40
|
+
const { name, url } = resolveProject(repoCommits[0], projectMap);
|
|
41
|
+
const label = mapped
|
|
42
|
+
? `${repo} — part of "${name}" (${url})`
|
|
43
|
+
: `${repo} — project: ${name} (${url})`;
|
|
44
|
+
|
|
45
|
+
sections.push(`## ${label}\n${repoCommits.map((c) => `- ${c.message}`).join('\n')}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return sections.join('\n\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a commit's repo to the best public link, in priority order:
|
|
53
|
+
* brand-config product URL > GitHub repo homepage field > GitHub repo URL.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} commit - A collected commit (carries owner/repo/homepage)
|
|
56
|
+
* @param {object} projectMap - repoName → { project, url }
|
|
57
|
+
* @returns {{ name: string, url: string }} Display name + link
|
|
58
|
+
*/
|
|
59
|
+
function resolveProject(commit, projectMap) {
|
|
60
|
+
const mapped = projectMap[commit.repo];
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: mapped ? mapped.project : commit.repo,
|
|
64
|
+
url: mapped?.url
|
|
65
|
+
|| commit.homepage
|
|
66
|
+
|| `https://github.com/${commit.owner}/${commit.repo}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Collect the unique project links actually touched by the commits, so the
|
|
72
|
+
* brief and Ghostii's `links` param only carry URLs grounded in real work.
|
|
73
|
+
*
|
|
74
|
+
* @param {Array} commits - Collected commits
|
|
75
|
+
* @param {object} projectMap - repoName → { project, url }
|
|
76
|
+
* @returns {Array<{ name: string, url: string }>} Unique projects
|
|
77
|
+
*/
|
|
78
|
+
function collectProjectLinks(commits, projectMap) {
|
|
79
|
+
const seen = new Map();
|
|
80
|
+
|
|
81
|
+
for (const commit of commits) {
|
|
82
|
+
const { name, url } = resolveProject(commit, projectMap);
|
|
83
|
+
|
|
84
|
+
if (!seen.has(url)) {
|
|
85
|
+
seen.set(url, { name, url });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return [...seen.values()];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build the Ghostii brief (description). Hard 2KB cap — assembled in priority
|
|
94
|
+
* order (task > blocklist > voice) so only the voice section ever gets truncated.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} params - { brandConfig, days }
|
|
97
|
+
* @returns {string} Brief within Ghostii's description cap
|
|
98
|
+
*/
|
|
99
|
+
function buildBrief({ brandConfig, days }) {
|
|
100
|
+
const { brand } = brandConfig;
|
|
101
|
+
const ghostii = brandConfig.devlog?.providers?.ghostii || {};
|
|
102
|
+
// Voice: the newsletter generator's instructions blob when the brand's
|
|
103
|
+
// omega.json5 carries one (it's @omega.js/backend data, not manager config — presence is
|
|
104
|
+
// genuinely uncertain), else the brand description.
|
|
105
|
+
const voice = brandConfig.marketing?.newsletter?.content?.[0]?.instructions
|
|
106
|
+
|| brand.description
|
|
107
|
+
|| '';
|
|
108
|
+
|
|
109
|
+
const task = `First-person devlog post for ${brand.name} (${brand.url}): the founder's honest recap of the last ${days} days of real development work across his product portfolio. Base the article ONLY on the commit digest provided as source content — never invent work that isn't there. Weave a narrative (what shipped, why it matters, lessons learned); skip trivial chores like version bumps and dependency updates.
|
|
110
|
+
|
|
111
|
+
Title style: short, punchy, and specific — like a git commit summary of the period's most interesting work (e.g. "Auth Round Trips and Quieter Cron Jobs"). Max ~8 words. NEVER use time-window phrasing ("Five Days of...", "A Week of...") — the post is dated already.`;
|
|
112
|
+
|
|
113
|
+
const blocklist = ghostii.excludeTopics.length
|
|
114
|
+
? `NEVER discuss, mention, or allude to: ${ghostii.excludeTopics.join('; ')}.`
|
|
115
|
+
: '';
|
|
116
|
+
|
|
117
|
+
// Project URLs go through Ghostii's `links` param (its allocator assigns them
|
|
118
|
+
// to sections natively) — keeping them out of the brief keeps it light enough
|
|
119
|
+
// for the outline step's reasoning budget.
|
|
120
|
+
const fixed = [task, blocklist].filter(Boolean).join('\n\n');
|
|
121
|
+
// Cap the voice excerpt well below the 2KB description limit — a lean brief
|
|
122
|
+
// keeps the outline step's reasoning budget in check.
|
|
123
|
+
const remaining = Math.min(MAX_DESCRIPTION_LENGTH - fixed.length - '\n\nVoice: '.length, 600);
|
|
124
|
+
|
|
125
|
+
if (remaining < 50) {
|
|
126
|
+
console.log(`${chalk.yellow('⚠')} Brief is at Ghostii's 2KB cap — voice instructions dropped`);
|
|
127
|
+
return fixed.slice(0, MAX_DESCRIPTION_LENGTH);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return voice
|
|
131
|
+
? `${fixed}\n\nVoice: ${voice.slice(0, remaining)}`
|
|
132
|
+
: fixed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate a devlog post via the configured provider (Ghostii).
|
|
137
|
+
*
|
|
138
|
+
* @param {object} params - { brandConfig, commits, projectMap, days, write? }
|
|
139
|
+
* (`write` is the article writer — tests inject a fake; prod uses Ghostii)
|
|
140
|
+
* @returns {Promise<{title, slug, description, tags, categories, body, headerImageUrl}>}
|
|
141
|
+
*/
|
|
142
|
+
async function generatePost({ brandConfig, commits, projectMap, days, write = writeArticle }) {
|
|
143
|
+
const writer = chosenProvider(brandConfig.devlog?.providers);
|
|
144
|
+
|
|
145
|
+
if (writer !== 'ghostii') {
|
|
146
|
+
throw new Error(`Unknown devlog writer: ${writer} (only 'ghostii' is supported — set devlog.providers.ghostii)`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ghostii = brandConfig.devlog.providers.ghostii;
|
|
150
|
+
|
|
151
|
+
const digest = buildDigest(commits, projectMap);
|
|
152
|
+
const projectLinks = collectProjectLinks(commits, projectMap);
|
|
153
|
+
const brief = buildBrief({ brandConfig, days });
|
|
154
|
+
|
|
155
|
+
if (digest.length > MAX_SOURCE_CONTENT_LENGTH) {
|
|
156
|
+
console.log(`${chalk.yellow('⚠')} Digest is ${digest.length} chars — Ghostii only reads the first ${MAX_SOURCE_CONTENT_LENGTH} (later repos get cut)`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const article = await write({
|
|
160
|
+
brandConfig,
|
|
161
|
+
description: brief,
|
|
162
|
+
links: projectLinks.map((p) => p.url),
|
|
163
|
+
sourceContent: digest,
|
|
164
|
+
overrides: ghostii.overrides,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Prefer the structured blocks (clean title/body separation); fall back to
|
|
168
|
+
// Ghostii's flat fields when blocks are absent.
|
|
169
|
+
const post = blocksToPost(article.json);
|
|
170
|
+
const title = post.title || article.title || '';
|
|
171
|
+
const body = post.body || article.body || '';
|
|
172
|
+
|
|
173
|
+
if (!title.trim() || !body.trim()) {
|
|
174
|
+
throw new Error('Ghostii response missing title or body');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
title: title.trim(),
|
|
179
|
+
slug: slugify(title),
|
|
180
|
+
description: (article.description || '').trim(),
|
|
181
|
+
tags: Array.isArray(article.keywords) ? article.keywords : [],
|
|
182
|
+
categories: Array.isArray(article.categories) ? article.categories : [],
|
|
183
|
+
body: body.trim(),
|
|
184
|
+
headerImageUrl: post.headerImageUrl || article.headerImageUrl || '',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Slugify a title: lowercase, alphanumerics + hyphens only.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} input - Title text
|
|
192
|
+
* @returns {string} URL slug
|
|
193
|
+
*/
|
|
194
|
+
function slugify(input) {
|
|
195
|
+
return input
|
|
196
|
+
.toLowerCase()
|
|
197
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
198
|
+
.replace(/^-+|-+$/g, '');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { generatePost };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ghostii API client — mirror of @omega.js/backend's Ghostii client
|
|
3
|
+
* (@omega.js/backend src/manager/libraries/content/ghostii.js). The SSOT for
|
|
4
|
+
* the request shape is @omega.js/backend; keep this in sync when it changes.
|
|
5
|
+
*
|
|
6
|
+
* Raw Cloud Functions URL (not api.ghostii.ai) to get the 5-min function
|
|
7
|
+
* timeout instead of Firebase Hosting's hard 60s proxy timeout.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const GHOSTII_WRITE_URL = 'https://us-central1-ghostii.cloudfunctions.net/writeGlobal/write/article';
|
|
11
|
+
|
|
12
|
+
// Ghostii API caps
|
|
13
|
+
const MAX_DESCRIPTION_LENGTH = 2000;
|
|
14
|
+
const MAX_SOURCE_CONTENT_LENGTH = 16000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* node-powertools' random({ mode: 'gaussian' }) equivalent — the mean of two
|
|
18
|
+
* uniform draws biases toward the middle of the range.
|
|
19
|
+
*
|
|
20
|
+
* @param {number} min - Inclusive lower bound
|
|
21
|
+
* @param {number} max - Inclusive upper bound
|
|
22
|
+
* @returns {number} Integer in [min, max]
|
|
23
|
+
*/
|
|
24
|
+
function gaussianRandomInt(min, max) {
|
|
25
|
+
const draw = () => min + Math.random() * (max - min);
|
|
26
|
+
|
|
27
|
+
return Math.round((draw() + draw()) / 2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate an article via the Ghostii API.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} args
|
|
34
|
+
* @param {object} args.brandConfig - Merged brand config
|
|
35
|
+
* @param {string} args.description - Article brief (truncated to Ghostii's 2KB cap)
|
|
36
|
+
* @param {string[]} [args.links] - URLs to inject into the article body
|
|
37
|
+
* @param {string} [args.sourceContent] - Reference text (the commit digest; 16KB cap)
|
|
38
|
+
* @param {object} [args.overrides] - Ghostii API param overrides (length, research, images, ...)
|
|
39
|
+
* @returns {Promise<object>} Ghostii's generic article response
|
|
40
|
+
* ({ title, description, body, json, headerImageUrl, categories, keywords, ... })
|
|
41
|
+
*/
|
|
42
|
+
async function writeArticle({ brandConfig, description, links = [], sourceContent, overrides = {} }) {
|
|
43
|
+
if (!process.env.OMEGA_ADMIN_KEY) {
|
|
44
|
+
throw new Error('OMEGA_ADMIN_KEY is not set — required to call the Ghostii API (brand or company .env)');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { brand } = brandConfig;
|
|
48
|
+
|
|
49
|
+
const body = {
|
|
50
|
+
// Ghostii's LIVE wire contract: its production backend runs legacy BEM,
|
|
51
|
+
// whose auth reads exactly this payload field. Renames to the omega
|
|
52
|
+
// spelling only when Ghostii itself migrates to the new stack — never
|
|
53
|
+
// "fix" unilaterally (it would break devlog publishing).
|
|
54
|
+
backendManagerKey: process.env.OMEGA_ADMIN_KEY,
|
|
55
|
+
keywords: overrides.keywords || [],
|
|
56
|
+
description: description.slice(0, MAX_DESCRIPTION_LENGTH),
|
|
57
|
+
insertLinks: overrides.insertLinks ?? true,
|
|
58
|
+
research: overrides.research ?? true,
|
|
59
|
+
insertImages: overrides.insertImages ?? true,
|
|
60
|
+
length: overrides.length || 'long',
|
|
61
|
+
maxLinks: overrides.maxLinks || 6,
|
|
62
|
+
headerImageUrl: overrides.headerImageUrl || 'unsplash',
|
|
63
|
+
url: brand.url,
|
|
64
|
+
sectionQuantity: overrides.sectionQuantity || gaussianRandomInt(3, 6),
|
|
65
|
+
feedUrl: overrides.feedUrl || `${brand.url}/feeds/posts.json`,
|
|
66
|
+
links,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (sourceContent) {
|
|
70
|
+
body.sourceContent = sourceContent.slice(0, MAX_SOURCE_CONTENT_LENGTH);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const response = await fetch(GHOSTII_WRITE_URL, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
signal: AbortSignal.timeout(180000),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const detail = (await response.text().catch(() => '')).slice(0, 300);
|
|
82
|
+
throw new Error(`Ghostii API ${response.status}: ${detail || response.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return response.json();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Transform Ghostii's JSON block array into a clean post shape.
|
|
90
|
+
* Mirror of @omega.js/backend's blocksToPost(): title ← first heading-1, headerImageUrl ←
|
|
91
|
+
* first image block, body ← every remaining block joined as markdown (no
|
|
92
|
+
* title embedded).
|
|
93
|
+
*
|
|
94
|
+
* @param {Array} json - Ghostii's block array ([{ name, content }])
|
|
95
|
+
* @returns {{ title: string, headerImageUrl: string, body: string }}
|
|
96
|
+
*/
|
|
97
|
+
function blocksToPost(json) {
|
|
98
|
+
const blocks = Array.isArray(json) ? json : [];
|
|
99
|
+
|
|
100
|
+
const titleBlock = blocks.find((b) => b.name === 'heading-1');
|
|
101
|
+
const imageBlock = blocks.find((b) => b.name === 'image');
|
|
102
|
+
|
|
103
|
+
const title = (titleBlock?.content || '').replace(/^#+\s*/, '').trim();
|
|
104
|
+
|
|
105
|
+
const imageMatch = (imageBlock?.content || '').match(/\((.*?)\)\s*$/);
|
|
106
|
+
const headerImageUrl = imageMatch ? imageMatch[1] : '';
|
|
107
|
+
|
|
108
|
+
const body = blocks
|
|
109
|
+
.filter((b) => b !== titleBlock && b !== imageBlock)
|
|
110
|
+
.map((b) => b.content)
|
|
111
|
+
.join('\n\n')
|
|
112
|
+
.trim();
|
|
113
|
+
|
|
114
|
+
return { title, headerImageUrl, body };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
writeArticle,
|
|
119
|
+
blocksToPost,
|
|
120
|
+
MAX_DESCRIPTION_LENGTH,
|
|
121
|
+
MAX_SOURCE_CONTENT_LENGTH,
|
|
122
|
+
};
|