@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.
Files changed (416) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/LICENSE +98 -0
  3. package/README.md +165 -0
  4. package/bin/omega +2 -0
  5. package/bin/omg +2 -0
  6. package/claude-plugin/.claude-plugin/plugin.json +11 -0
  7. package/claude-plugin/.mcp.json +9 -0
  8. package/claude-plugin/README.md +139 -0
  9. package/claude-plugin/hooks/gate/mark.sh +58 -0
  10. package/claude-plugin/hooks/gate/run.sh +97 -0
  11. package/claude-plugin/hooks/guard/run.sh +249 -0
  12. package/claude-plugin/hooks/hooks.json +63 -0
  13. package/claude-plugin/hooks/inject/run.sh +127 -0
  14. package/claude-plugin/hooks/lib/omega-gate.sh +20 -0
  15. package/claude-plugin/hooks/lib/omega-scope.sh +41 -0
  16. package/claude-plugin/hooks/lib/omega-skills.sh +209 -0
  17. package/claude-plugin/hooks/quality/run.sh +132 -0
  18. package/claude-plugin/hooks/shape/run.sh +45 -0
  19. package/claude-plugin/mcp-router-launch.js +29 -0
  20. package/claude-plugin/skills/README.md +48 -0
  21. package/claude-plugin/skills/accessibility/SKILL.md +31 -0
  22. package/claude-plugin/skills/analytics/SKILL.md +30 -0
  23. package/claude-plugin/skills/backend/SKILL.md +25 -0
  24. package/claude-plugin/skills/brandcheck/SKILL.md +29 -0
  25. package/claude-plugin/skills/browser/SKILL.md +64 -0
  26. package/claude-plugin/skills/client/SKILL.md +24 -0
  27. package/claude-plugin/skills/desktop/SKILL.md +25 -0
  28. package/claude-plugin/skills/extension/SKILL.md +25 -0
  29. package/claude-plugin/skills/main/SKILL.md +54 -0
  30. package/claude-plugin/skills/manager/SKILL.md +25 -0
  31. package/claude-plugin/skills/seo/SKILL.md +36 -0
  32. package/claude-plugin/skills/theme/SKILL.md +33 -0
  33. package/claude-plugin/skills/web/SKILL.md +29 -0
  34. package/dist/cli-run.js +34 -0
  35. package/dist/cli.js +29 -0
  36. package/dist/commands/build.js +17 -0
  37. package/dist/commands/clean.js +16 -0
  38. package/dist/commands/company.js +46 -0
  39. package/dist/commands/deploy.js +143 -0
  40. package/dist/commands/dev.js +437 -0
  41. package/dist/commands/devlog.js +25 -0
  42. package/dist/commands/help.js +48 -0
  43. package/dist/commands/manage.js +53 -0
  44. package/dist/commands/migrate.js +76 -0
  45. package/dist/commands/onboard.js +31 -0
  46. package/dist/commands/pipeline.js +319 -0
  47. package/dist/commands/test.js +380 -0
  48. package/dist/commands/update.js +128 -0
  49. package/dist/commands/version.js +8 -0
  50. package/dist/company-init.js +167 -0
  51. package/dist/company.js +283 -0
  52. package/dist/config.js +1359 -0
  53. package/dist/devlog/index.js +226 -0
  54. package/dist/devlog/lib/collect-commits.js +136 -0
  55. package/dist/devlog/lib/generate-post.js +201 -0
  56. package/dist/devlog/lib/ghostii.js +122 -0
  57. package/dist/devlog/lib/project-map.js +98 -0
  58. package/dist/devlog/lib/publish-website.js +88 -0
  59. package/dist/index.js +45 -0
  60. package/dist/lib/agents-md.js +201 -0
  61. package/dist/lib/analytics-secret.js +22 -0
  62. package/dist/lib/argv.js +21 -0
  63. package/dist/lib/auth-admin.js +120 -0
  64. package/dist/lib/automation-client.js +210 -0
  65. package/dist/lib/backend-marketing.js +49 -0
  66. package/dist/lib/brand.js +240 -0
  67. package/dist/lib/bundle-id.js +53 -0
  68. package/dist/lib/claude-settings.js +125 -0
  69. package/dist/lib/company-scaffold.js +213 -0
  70. package/dist/lib/company.js +227 -0
  71. package/dist/lib/config-flow.js +350 -0
  72. package/dist/lib/config-write.js +41 -0
  73. package/dist/lib/custom-target.js +75 -0
  74. package/dist/lib/domain-utils.js +19 -0
  75. package/dist/lib/duration.js +39 -0
  76. package/dist/lib/env-order.js +236 -0
  77. package/dist/lib/env-secret.js +78 -0
  78. package/dist/lib/firestore-rest.js +326 -0
  79. package/dist/lib/framework-bin.js +156 -0
  80. package/dist/lib/gitignore.js +63 -0
  81. package/dist/lib/google-auth.js +495 -0
  82. package/dist/lib/google-token.js +66 -0
  83. package/dist/lib/jwt.js +31 -0
  84. package/dist/lib/legacy-oauth.js +59 -0
  85. package/dist/lib/node-version.js +127 -0
  86. package/dist/lib/owner-plan.js +78 -0
  87. package/dist/lib/package-scripts.js +152 -0
  88. package/dist/lib/preflight.js +470 -0
  89. package/dist/lib/product-create.js +144 -0
  90. package/dist/lib/run-command.js +74 -0
  91. package/dist/lib/run-gates.js +70 -0
  92. package/dist/lib/run-output.js +42 -0
  93. package/dist/lib/run-summary.js +384 -0
  94. package/dist/lib/scaffold.js +427 -0
  95. package/dist/lib/service-input.js +207 -0
  96. package/dist/lib/service-runner.js +466 -0
  97. package/dist/lib/stale.js +24 -0
  98. package/dist/lib/target-selection.js +155 -0
  99. package/dist/lib/verb-fanout.js +128 -0
  100. package/dist/lib/verify-live.js +188 -0
  101. package/dist/manage.js +311 -0
  102. package/dist/omega-bin.js +7 -0
  103. package/dist/onboard.js +532 -0
  104. package/dist/services/account/ensure/users.js +259 -0
  105. package/dist/services/account/index.js +97 -0
  106. package/dist/services/account/lib/backend-client.js +107 -0
  107. package/dist/services/account/lib/password.js +36 -0
  108. package/dist/services/account/lib/resolve-password.js +100 -0
  109. package/dist/services/advertising/ensure/sites.js +101 -0
  110. package/dist/services/advertising/index.js +101 -0
  111. package/dist/services/advertising/lib/adsense-api.js +41 -0
  112. package/dist/services/ai/ensure/keys.js +21 -0
  113. package/dist/services/ai/index.js +36 -0
  114. package/dist/services/analytics/ensure/google-firebase-link.js +230 -0
  115. package/dist/services/analytics/ensure/google-streams.js +256 -0
  116. package/dist/services/analytics/ensure/meta-pixel.js +22 -0
  117. package/dist/services/analytics/ensure/tiktok-pixel.js +22 -0
  118. package/dist/services/analytics/index.js +134 -0
  119. package/dist/services/analytics/lib/analytics-api.js +156 -0
  120. package/dist/services/analytics/lib/meta-api.js +83 -0
  121. package/dist/services/analytics/lib/pixel-account.js +78 -0
  122. package/dist/services/analytics/lib/pixel-provision.js +152 -0
  123. package/dist/services/analytics/lib/pixel-specs.js +57 -0
  124. package/dist/services/analytics/lib/pixel-token.js +88 -0
  125. package/dist/services/analytics/lib/property-flow.js +77 -0
  126. package/dist/services/analytics/lib/tiktok-api.js +157 -0
  127. package/dist/services/analytics/lib/tiktok-auth.js +153 -0
  128. package/dist/services/assets/ensure/logo-gen.js +68 -0
  129. package/dist/services/assets/index.js +109 -0
  130. package/dist/services/assets/lib/assets-config.js +168 -0
  131. package/dist/services/assets/lib/brandmark-api.js +160 -0
  132. package/dist/services/assets/lib/derived.js +113 -0
  133. package/dist/services/assets/lib/font-loader.js +67 -0
  134. package/dist/services/assets/lib/reconcile.js +122 -0
  135. package/dist/services/assets/lib/reset.js +118 -0
  136. package/dist/services/assets/lib/svg-logo-generator.js +154 -0
  137. package/dist/services/assets/lib/svg-to-black.js +32 -0
  138. package/dist/services/assets/write/favicons.js +82 -0
  139. package/dist/services/assets/write/icons.js +64 -0
  140. package/dist/services/assets/write/process.js +96 -0
  141. package/dist/services/assets/write/reconcile.js +30 -0
  142. package/dist/services/assets/write/social-icons.js +108 -0
  143. package/dist/services/assets/write/templates.js +345 -0
  144. package/dist/services/bookmark/ensure/sync.js +255 -0
  145. package/dist/services/bookmark/index.js +17 -0
  146. package/dist/services/campaigns/ensure/contact-person.js +40 -0
  147. package/dist/services/campaigns/ensure/custom-fields.js +84 -0
  148. package/dist/services/campaigns/ensure/domain-auth.js +92 -0
  149. package/dist/services/campaigns/ensure/event-webhook.js +89 -0
  150. package/dist/services/campaigns/ensure/link-branding.js +187 -0
  151. package/dist/services/campaigns/ensure/list.js +50 -0
  152. package/dist/services/campaigns/ensure/segments.js +109 -0
  153. package/dist/services/campaigns/ensure/sender-identity.js +153 -0
  154. package/dist/services/campaigns/ensure/unsubscribe-groups.js +116 -0
  155. package/dist/services/campaigns/index.js +79 -0
  156. package/dist/services/campaigns/lib/dns-sync.js +81 -0
  157. package/dist/services/campaigns/lib/segment-query.js +138 -0
  158. package/dist/services/campaigns/lib/sendgrid-api.js +287 -0
  159. package/dist/services/captcha/ensure/site-key.js +83 -0
  160. package/dist/services/captcha/index.js +78 -0
  161. package/dist/services/captcha/lib/console-url.js +45 -0
  162. package/dist/services/captcha/lib/recaptcha-api.js +36 -0
  163. package/dist/services/certificates/ensure/api-key.js +22 -0
  164. package/dist/services/certificates/ensure/bundle-ids.js +115 -0
  165. package/dist/services/certificates/ensure/certificates.js +187 -0
  166. package/dist/services/certificates/ensure/profiles.js +143 -0
  167. package/dist/services/certificates/index.js +225 -0
  168. package/dist/services/certificates/lib/apple-api.js +183 -0
  169. package/dist/services/certificates/lib/certificate-manager.js +252 -0
  170. package/dist/services/certificates/lib/identifier-manager.js +107 -0
  171. package/dist/services/certificates/lib/keychain.js +135 -0
  172. package/dist/services/certificates/lib/manual-walkthrough.js +165 -0
  173. package/dist/services/certificates/lib/profile-manager.js +115 -0
  174. package/dist/services/chat/data/baseline-knowledge.md +96 -0
  175. package/dist/services/chat/ensure/chat.js +106 -0
  176. package/dist/services/chat/ensure/user.js +45 -0
  177. package/dist/services/chat/index.js +139 -0
  178. package/dist/services/chat/lib/baseline-knowledge.js +137 -0
  179. package/dist/services/cloud/ensure/authentication.js +267 -0
  180. package/dist/services/cloud/ensure/billing.js +120 -0
  181. package/dist/services/cloud/ensure/cloud-messaging.js +105 -0
  182. package/dist/services/cloud/ensure/database.js +60 -0
  183. package/dist/services/cloud/ensure/firestore.js +79 -0
  184. package/dist/services/cloud/ensure/functions.js +49 -0
  185. package/dist/services/cloud/ensure/hosting.js +398 -0
  186. package/dist/services/cloud/ensure/oauth-consent.js +248 -0
  187. package/dist/services/cloud/ensure/project-settings.js +72 -0
  188. package/dist/services/cloud/ensure/sdk-config.js +95 -0
  189. package/dist/services/cloud/ensure/service-account.js +152 -0
  190. package/dist/services/cloud/ensure/services.js +103 -0
  191. package/dist/services/cloud/ensure/storage.js +47 -0
  192. package/dist/services/cloud/index.js +130 -0
  193. package/dist/services/cloud/lib/access-heal.js +130 -0
  194. package/dist/services/cloud/lib/firebase-api.js +662 -0
  195. package/dist/services/cloud/lib/project-flow.js +102 -0
  196. package/dist/services/directory/ensure/entry.js +48 -0
  197. package/dist/services/directory/index.js +64 -0
  198. package/dist/services/directory/lib/blocks.js +104 -0
  199. package/dist/services/disperse/index.js +45 -0
  200. package/dist/services/disperse/write/certs.js +117 -0
  201. package/dist/services/domain/ensure/nameservers.js +179 -0
  202. package/dist/services/domain/index.js +69 -0
  203. package/dist/services/domain/lib/namecheap-api.js +167 -0
  204. package/dist/services/domain/lib/registrars.js +46 -0
  205. package/dist/services/domain/lib/whitelist-walkthrough.js +76 -0
  206. package/dist/services/edge/ensure/cache-rules.js +105 -0
  207. package/dist/services/edge/ensure/dns-records.js +304 -0
  208. package/dist/services/edge/ensure/email-routing.js +292 -0
  209. package/dist/services/edge/ensure/rules-configuration.js +98 -0
  210. package/dist/services/edge/ensure/rules-managed-transforms.js +101 -0
  211. package/dist/services/edge/ensure/rules-redirect.js +107 -0
  212. package/dist/services/edge/ensure/rules-response-headers.js +90 -0
  213. package/dist/services/edge/ensure/rules-security.js +142 -0
  214. package/dist/services/edge/ensure/speed-scheduled-tests.js +98 -0
  215. package/dist/services/edge/ensure/workers.js +219 -0
  216. package/dist/services/edge/ensure/zone-settings.js +108 -0
  217. package/dist/services/edge/ensure/zone.js +183 -0
  218. package/dist/services/edge/index.js +73 -0
  219. package/dist/services/edge/lib/cloudflare-api.js +84 -0
  220. package/dist/services/edge/lib/dns-records-helpers.js +518 -0
  221. package/dist/services/edge/lib/read-cache.js +16 -0
  222. package/dist/services/edge/lib/ruleset-helper.js +108 -0
  223. package/dist/services/edge/workers/omega-api-proxy.js +43 -0
  224. package/dist/services/email/data/baseline-filter.md +4 -0
  225. package/dist/services/email/data/baseline-knowledge.md +86 -0
  226. package/dist/services/email/ensure/agent.js +107 -0
  227. package/dist/services/email/ensure/user.js +45 -0
  228. package/dist/services/email/index.js +138 -0
  229. package/dist/services/email/lib/baseline.js +82 -0
  230. package/dist/services/email/lib/knowledge-file.js +53 -0
  231. package/dist/services/forms/ensure/form.js +46 -0
  232. package/dist/services/forms/ensure/user.js +45 -0
  233. package/dist/services/forms/index.js +138 -0
  234. package/dist/services/migrations/ensure/notifications.js +189 -0
  235. package/dist/services/migrations/ensure/orders.js +34 -0
  236. package/dist/services/migrations/ensure/payment-provider.js +132 -0
  237. package/dist/services/migrations/ensure/payments-intents.js +34 -0
  238. package/dist/services/migrations/ensure/state-retirement.js +266 -0
  239. package/dist/services/migrations/ensure/targets-rename.js +124 -0
  240. package/dist/services/migrations/ensure/users.js +1176 -0
  241. package/dist/services/migrations/index.js +101 -0
  242. package/dist/services/migrations/lib/attribution-touch.js +64 -0
  243. package/dist/services/migrations/lib/ensure-metadata.js +144 -0
  244. package/dist/services/migrations/lib/migration-runner.js +473 -0
  245. package/dist/services/migrations/lib/sanitize-strings.js +112 -0
  246. package/dist/services/migrations/lib/schema-validator.js +154 -0
  247. package/dist/services/monitoring/ensure/dsn.js +54 -0
  248. package/dist/services/monitoring/ensure/projects.js +135 -0
  249. package/dist/services/monitoring/index.js +54 -0
  250. package/dist/services/monitoring/lib/sentry-api.js +94 -0
  251. package/dist/services/newsletter/ensure/custom-fields.js +90 -0
  252. package/dist/services/newsletter/ensure/publication.js +94 -0
  253. package/dist/services/newsletter/ensure/segments.js +140 -0
  254. package/dist/services/newsletter/ensure/webhook.js +108 -0
  255. package/dist/services/newsletter/index.js +64 -0
  256. package/dist/services/newsletter/lib/beehiiv-api.js +124 -0
  257. package/dist/services/newsletter/lib/segment-automation.js +577 -0
  258. package/dist/services/payment/ensure/chargebee-account.js +30 -0
  259. package/dist/services/payment/ensure/chargebee-products.js +359 -0
  260. package/dist/services/payment/ensure/chargebee-webhook.js +113 -0
  261. package/dist/services/payment/ensure/paypal-account.js +36 -0
  262. package/dist/services/payment/ensure/paypal-products.js +374 -0
  263. package/dist/services/payment/ensure/paypal-webhook.js +128 -0
  264. package/dist/services/payment/ensure/stripe-account.js +93 -0
  265. package/dist/services/payment/ensure/stripe-disputes.js +56 -0
  266. package/dist/services/payment/ensure/stripe-products.js +245 -0
  267. package/dist/services/payment/ensure/stripe-radar.js +74 -0
  268. package/dist/services/payment/ensure/stripe-webhook.js +130 -0
  269. package/dist/services/payment/index.js +163 -0
  270. package/dist/services/payment/lib/chargebee-api.js +338 -0
  271. package/dist/services/payment/lib/payment-utils.js +116 -0
  272. package/dist/services/payment/lib/paypal-api.js +354 -0
  273. package/dist/services/payment/lib/provider-setup.js +134 -0
  274. package/dist/services/payment/lib/stripe-api.js +152 -0
  275. package/dist/services/repo/ensure/org.js +70 -0
  276. package/dist/services/repo/ensure/pages.js +72 -0
  277. package/dist/services/repo/ensure/repo.js +90 -0
  278. package/dist/services/repo/index.js +52 -0
  279. package/dist/services/repo/lib/github-api.js +196 -0
  280. package/dist/services/search/ensure/ga-link.js +57 -0
  281. package/dist/services/search/ensure/property.js +137 -0
  282. package/dist/services/search/ensure/sitemaps.js +70 -0
  283. package/dist/services/search/index.js +61 -0
  284. package/dist/services/search/lib/search-console-api.js +92 -0
  285. package/dist/services/seo/ensure/github-repos.js +271 -0
  286. package/dist/services/seo/index.js +55 -0
  287. package/dist/services/seo/lib/gh-api.js +161 -0
  288. package/dist/services/seo/templates/developer-tool/.github/workflows/maintenance.yml +25 -0
  289. package/dist/services/seo/templates/developer-tool/.nvmrc +1 -0
  290. package/dist/services/seo/templates/developer-tool/_README.md.js +121 -0
  291. package/dist/services/seo/templates/developer-tool/_package.json.js +33 -0
  292. package/dist/services/seo/templates/developer-tool/src/index.js +120 -0
  293. package/dist/services/seo/templates/index.js +57 -0
  294. package/dist/services/server/ensure/brands.js +63 -0
  295. package/dist/services/server/index.js +42 -0
  296. package/dist/services/testing/ensure/target-checks.js +97 -0
  297. package/dist/services/testing/index.js +11 -0
  298. package/dist/services/testing/lib/checks.js +467 -0
  299. package/dist/services/update/index.js +20 -0
  300. package/dist/services/update/lib/cache.js +59 -0
  301. package/dist/services/update/lib/fingerprint.js +157 -0
  302. package/dist/services/update/write/targets.js +206 -0
  303. package/dist/services/workspace/ensure/agents.js +42 -0
  304. package/dist/services/workspace/ensure/claude-settings.js +34 -0
  305. package/dist/services/workspace/ensure/config.js +54 -0
  306. package/dist/services/workspace/ensure/defaults.js +60 -0
  307. package/dist/services/workspace/ensure/env-keys.js +55 -0
  308. package/dist/services/workspace/ensure/env-order.js +66 -0
  309. package/dist/services/workspace/ensure/env-rules.js +97 -0
  310. package/dist/services/workspace/ensure/gitignore.js +18 -0
  311. package/dist/services/workspace/ensure/scripts.js +126 -0
  312. package/dist/services/workspace/ensure/structure.js +97 -0
  313. package/dist/services/workspace/ensure/translation-sdk.js +148 -0
  314. package/dist/services/workspace/ensure/workflows.js +54 -0
  315. package/dist/services/workspace/index.js +8 -0
  316. package/dist/vendor/account/engine.js +182 -0
  317. package/dist/vendor/account/features.js +220 -0
  318. package/dist/vendor/account/index.js +53 -0
  319. package/dist/vendor/account/schema.js +272 -0
  320. package/dist/vendor/account/subscription.js +38 -0
  321. package/dist/vendor/config/company.js +31 -0
  322. package/dist/vendor/config/defaults.js +173 -0
  323. package/dist/vendor/config/demo.js +18 -0
  324. package/dist/vendor/config/desktop-artifacts.js +110 -0
  325. package/dist/vendor/config/edit.js +769 -0
  326. package/dist/vendor/config/env-delivery.js +145 -0
  327. package/dist/vendor/config/env-rules.js +93 -0
  328. package/dist/vendor/config/env-schema.js +1078 -0
  329. package/dist/vendor/config/env.js +445 -0
  330. package/dist/vendor/config/hooks.js +97 -0
  331. package/dist/vendor/config/index.js +237 -0
  332. package/dist/vendor/config/instances.js +208 -0
  333. package/dist/vendor/config/load.js +490 -0
  334. package/dist/vendor/config/merge.js +68 -0
  335. package/dist/vendor/config/order.js +139 -0
  336. package/dist/vendor/config/ports.js +374 -0
  337. package/dist/vendor/config/providers.js +32 -0
  338. package/dist/vendor/config/repo.js +142 -0
  339. package/dist/vendor/config/retired-keys.js +430 -0
  340. package/dist/vendor/config/schema.js +1610 -0
  341. package/dist/vendor/config/secrets.js +50 -0
  342. package/dist/vendor/config/seed.js +34 -0
  343. package/dist/vendor/config/site-global.js +205 -0
  344. package/dist/vendor/config/validate.js +554 -0
  345. package/dist/vendor/config/winback.js +61 -0
  346. package/dist/vendor/devkit/attach-log-file.js +262 -0
  347. package/dist/vendor/devkit/certs.js +199 -0
  348. package/dist/vendor/devkit/ci-workflows.js +520 -0
  349. package/dist/vendor/devkit/cli-router.js +156 -0
  350. package/dist/vendor/devkit/command-path.js +46 -0
  351. package/dist/vendor/devkit/deploy-record.js +180 -0
  352. package/dist/vendor/devkit/flows.js +317 -0
  353. package/dist/vendor/devkit/local-https.js +360 -0
  354. package/dist/vendor/devkit/local.js +1905 -0
  355. package/dist/vendor/devkit/logger.js +128 -0
  356. package/dist/vendor/devkit/omega-bin.js +345 -0
  357. package/dist/vendor/devkit/prompt.js +187 -0
  358. package/dist/vendor/devkit/safe-install.js +18 -0
  359. package/dist/vendor/devkit/stop-signals.js +28 -0
  360. package/dist/vendor/devkit/test/scope.js +162 -0
  361. package/dist/vendor/devkit/translate/cache.js +84 -0
  362. package/dist/vendor/devkit/translate/engine.js +243 -0
  363. package/dist/vendor/devkit/translate/index.js +49 -0
  364. package/dist/vendor/devkit/translate/languages.js +134 -0
  365. package/dist/vendor/devkit/translate/providers.js +188 -0
  366. package/dist/vendor/devkit/update.js +569 -0
  367. package/docs/AGENTS.md +200 -0
  368. package/docs/account.md +45 -0
  369. package/docs/advertising.md +46 -0
  370. package/docs/ai.md +34 -0
  371. package/docs/analytics.md +69 -0
  372. package/docs/assets.md +57 -0
  373. package/docs/bookmark.md +26 -0
  374. package/docs/brand.md +151 -0
  375. package/docs/campaigns.md +96 -0
  376. package/docs/captcha.md +46 -0
  377. package/docs/certificates.md +68 -0
  378. package/docs/chat.md +47 -0
  379. package/docs/cloud.md +110 -0
  380. package/docs/company.md +64 -0
  381. package/docs/directory.md +140 -0
  382. package/docs/disperse.md +46 -0
  383. package/docs/domain.md +56 -0
  384. package/docs/edge.md +234 -0
  385. package/docs/email.md +42 -0
  386. package/docs/forms.md +48 -0
  387. package/docs/index.md +109 -0
  388. package/docs/migration.md +203 -0
  389. package/docs/migrations.md +56 -0
  390. package/docs/monitoring.md +41 -0
  391. package/docs/newsletter.md +48 -0
  392. package/docs/payment.md +83 -0
  393. package/docs/repo.md +48 -0
  394. package/docs/search.md +44 -0
  395. package/docs/seo.md +34 -0
  396. package/docs/server.md +37 -0
  397. package/docs/shared/agent-docs.md +89 -0
  398. package/docs/shared/analytics.md +612 -0
  399. package/docs/shared/brands.md +51 -0
  400. package/docs/shared/breaking-changes.md +497 -0
  401. package/docs/shared/config.md +1387 -0
  402. package/docs/shared/deploys.md +215 -0
  403. package/docs/shared/icons.md +201 -0
  404. package/docs/shared/local-dev.md +147 -0
  405. package/docs/shared/logging.md +202 -0
  406. package/docs/shared/monitoring.md +153 -0
  407. package/docs/shared/publishing.md +183 -0
  408. package/docs/shared/rulings.md +34 -0
  409. package/docs/shared/testing.md +147 -0
  410. package/docs/shared/theming.md +604 -0
  411. package/docs/shared/translation.md +291 -0
  412. package/docs/shared/updates.md +61 -0
  413. package/docs/testing.md +34 -0
  414. package/docs/update.md +36 -0
  415. package/docs/workspace.md +64 -0
  416. 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
+ };