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