@omega.js/desktop 0.53.0 → 0.54.1

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 (161) hide show
  1. package/README.md +38 -38
  2. package/dist/cli-run.js +4 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/cdp/client.js +1 -1
  5. package/dist/commands/cdp.js +1 -1
  6. package/dist/commands/clean.js +2 -3
  7. package/dist/commands/dev.js +25 -0
  8. package/dist/commands/lib/ensure-target.js +12 -17
  9. package/dist/commands/lib/migrate.js +17 -0
  10. package/dist/commands/logs.js +1 -1
  11. package/dist/commands/release.js +1 -1
  12. package/dist/commands/test.js +4 -4
  13. package/dist/commands/update.js +5 -4
  14. package/dist/defaults/.github/workflows/build.yml +18 -18
  15. package/dist/defaults/_.gitignore +0 -2
  16. package/dist/defaults/_mas/README.md +3 -3
  17. package/dist/defaults/config/certs/README.md +1 -1
  18. package/dist/defaults/config/omega.json5 +36 -36
  19. package/dist/defaults/docs/README.md +3 -3
  20. package/dist/defaults/gulpfile.js +1 -1
  21. package/dist/defaults/hooks/build/post.js +1 -1
  22. package/dist/defaults/hooks/build/pre.js +1 -1
  23. package/dist/defaults/hooks/notarize/post.js +2 -2
  24. package/dist/defaults/hooks/release/post.js +1 -1
  25. package/dist/defaults/hooks/release/pre.js +1 -1
  26. package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
  27. package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
  28. package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
  29. package/dist/defaults/src/integrations/context-menu/index.js +11 -11
  30. package/dist/defaults/src/integrations/menu/index.js +5 -5
  31. package/dist/defaults/src/integrations/tray/index.js +9 -9
  32. package/dist/defaults/src/main.js +2 -2
  33. package/dist/defaults/src/preload.js +1 -1
  34. package/dist/defaults/test/README.md +3 -3
  35. package/dist/defaults/test/_init.js +1 -1
  36. package/dist/gulp/tasks/audit.js +5 -8
  37. package/dist/lib/restart-manager/index.js +1 -1
  38. package/dist/lib/restart-manager/install.js +1 -1
  39. package/dist/lib/restart-manager/protocol.js +1 -1
  40. package/dist/main.js +4 -3
  41. package/dist/preload.js +1 -1
  42. package/dist/test/suites/build/audit.test.js +20 -7
  43. package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
  44. package/dist/test/suites/build/cli.test.js +28 -0
  45. package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
  46. package/dist/test/suites/build/defaults-scaffold.test.js +19 -5
  47. package/dist/test/suites/build/deploy-direct.test.js +7 -5
  48. package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
  49. package/dist/test/suites/build/deploy-hook.test.js +4 -2
  50. package/dist/test/suites/build/dev-verb.test.js +67 -0
  51. package/dist/test/suites/build/ensure-target.test.js +11 -3
  52. package/dist/test/suites/build/merge-line-files.test.js +6 -6
  53. package/dist/test/suites/build/migrate.test.js +29 -0
  54. package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
  55. package/dist/test/suites/build/runner-env-write.test.js +73 -0
  56. package/dist/test/suites/build/runner.test.js +9 -8
  57. package/dist/test/suites/build/setup-scripts.test.js +27 -0
  58. package/dist/test/suites/build/validate-config.test.js +13 -2
  59. package/dist/test/suites/build/verb-logs.test.js +20 -0
  60. package/dist/test/suites/renderer/window-desktop-surface.test.js +1 -1
  61. package/dist/utils/build-pipeline.js +4 -4
  62. package/dist/utils/runner-env.js +13 -28
  63. package/dist/vendor/config/company.js +46 -14
  64. package/dist/vendor/config/defaults.js +30 -7
  65. package/dist/vendor/config/edit.js +25 -3
  66. package/dist/vendor/config/env-delivery.js +1 -1
  67. package/dist/vendor/config/env-schema.js +3 -6
  68. package/dist/vendor/config/env.js +34 -22
  69. package/dist/vendor/config/index.js +13 -17
  70. package/dist/vendor/config/load.js +15 -7
  71. package/dist/vendor/config/repo.js +10 -27
  72. package/dist/vendor/config/schema-client.js +64 -0
  73. package/dist/vendor/config/schema-cloud.js +38 -0
  74. package/dist/vendor/config/schema-manager.js +118 -0
  75. package/dist/vendor/config/schema-overrides.js +68 -0
  76. package/dist/vendor/config/schema.js +99 -152
  77. package/dist/vendor/config/validate.js +97 -77
  78. package/dist/vendor/devkit/agents-md.js +233 -0
  79. package/dist/vendor/devkit/attach-log-file.js +15 -1
  80. package/dist/vendor/devkit/ci-workflows.js +30 -30
  81. package/dist/vendor/devkit/cli-router.js +13 -7
  82. package/dist/vendor/devkit/defaults-engine.js +9 -43
  83. package/dist/vendor/devkit/deploy-snapshot.js +44 -9
  84. package/dist/vendor/devkit/env-lines.js +183 -0
  85. package/dist/vendor/devkit/local.js +62 -10
  86. package/dist/vendor/devkit/lockfile.js +32 -13
  87. package/dist/vendor/devkit/logger.js +7 -2
  88. package/dist/vendor/devkit/merge-line-files.js +219 -176
  89. package/dist/vendor/devkit/omega-bin.js +208 -111
  90. package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
  91. package/dist/vendor/devkit/preludes/index.js +1 -0
  92. package/dist/vendor/devkit/target-picker.js +45 -0
  93. package/dist/vendor/devkit/test/dashed-files.js +37 -0
  94. package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
  95. package/dist/vendor/devkit/update.js +15 -15
  96. package/dist/vendor/devkit/verb-scripts.js +40 -0
  97. package/dist/vendor/devkit/verbs.js +170 -0
  98. package/package.json +18 -24
  99. package/dist/commands/install.js +0 -37
  100. package/dist/defaults/AGENTS.md +0 -119
  101. package/dist/defaults/CLAUDE.md +0 -1
  102. package/dist/vendor/config/env-retired.js +0 -137
  103. package/dist/vendor/config/retired-keys.js +0 -635
  104. package/docs/analytics.md +0 -140
  105. package/docs/app-state.md +0 -92
  106. package/docs/audit.md +0 -69
  107. package/docs/auth.md +0 -284
  108. package/docs/auto-updater.md +0 -243
  109. package/docs/boot-sequence.md +0 -44
  110. package/docs/build-system.md +0 -169
  111. package/docs/cdp-debugging.md +0 -169
  112. package/docs/common-mistakes.md +0 -21
  113. package/docs/config-schema.md +0 -120
  114. package/docs/context-menu.md +0 -112
  115. package/docs/context.md +0 -81
  116. package/docs/css.md +0 -84
  117. package/docs/deep-link.md +0 -186
  118. package/docs/environment-detection.md +0 -112
  119. package/docs/fontawesome.md +0 -109
  120. package/docs/hooks.md +0 -89
  121. package/docs/icons.md +0 -79
  122. package/docs/index.md +0 -328
  123. package/docs/installer-options.md +0 -165
  124. package/docs/ipc.md +0 -61
  125. package/docs/lib-modules.md +0 -53
  126. package/docs/logging.md +0 -227
  127. package/docs/menu.md +0 -160
  128. package/docs/releasing.md +0 -239
  129. package/docs/remote-config.md +0 -118
  130. package/docs/remote-scripts.md +0 -144
  131. package/docs/restart-manager.md +0 -144
  132. package/docs/runner.md +0 -290
  133. package/docs/sentry.md +0 -97
  134. package/docs/shared/agent-docs.md +0 -89
  135. package/docs/shared/analytics.md +0 -612
  136. package/docs/shared/brands.md +0 -57
  137. package/docs/shared/breaking-changes.md +0 -917
  138. package/docs/shared/config.md +0 -1948
  139. package/docs/shared/deploys.md +0 -341
  140. package/docs/shared/icons.md +0 -219
  141. package/docs/shared/local-dev.md +0 -167
  142. package/docs/shared/logging.md +0 -205
  143. package/docs/shared/monitoring.md +0 -167
  144. package/docs/shared/publishing.md +0 -187
  145. package/docs/shared/rulings.md +0 -34
  146. package/docs/shared/testing.md +0 -147
  147. package/docs/shared/theming.md +0 -629
  148. package/docs/shared/translation.md +0 -342
  149. package/docs/shared/updates.md +0 -61
  150. package/docs/signing.md +0 -293
  151. package/docs/startup.md +0 -142
  152. package/docs/storage.md +0 -59
  153. package/docs/templating.md +0 -101
  154. package/docs/test-boot-layer.md +0 -157
  155. package/docs/test-framework.md +0 -362
  156. package/docs/themes.md +0 -149
  157. package/docs/tooltips.md +0 -99
  158. package/docs/tray.md +0 -164
  159. package/docs/usage.md +0 -58
  160. package/docs/verts.md +0 -62
  161. package/docs/windows.md +0 -149
@@ -1,635 +0,0 @@
1
- /**
2
- * Retired-key detection (#142) — a key that was RENAMED outright in the
3
- * migration to omega.json5 is an error, never a silent no-op. There is no
4
- * dual-read anywhere in OMEGA, so a config still carrying `web_manager`
5
- * validated clean and quietly lost its auth policy, cookie consent and
6
- * chatsy settings: nothing reads that name.
7
- *
8
- * Like secrets.js this is a key-NAME test, walked at every depth (shared
9
- * level, inside a target entry, inside an array item): the rename moved
10
- * the key, not its home. Only unambiguous renames belong here: a name that
11
- * still exists as a legitimate key somewhere in the schema (`google`/`meta`
12
- * under `analytics.providers`) would false-positive and is left to the mapping
13
- * tables. `sentry` is the one that LOOKS like an omission and is not: its new
14
- * home is itself a `sentry` key (`monitoring.providers.sentry` — #485 moved
15
- * the web converter there too), so a name test would fire on the very shape
16
- * every mapping row points at.
17
- *
18
- * RETIRED_PATHS is the second half (#23): the de-branding rekey moved whole
19
- * top-level keys into role-shaped homes where the provider keeps its own
20
- * name (`slapform` → `forms.providers.slapform`), and folded two keys into
21
- * `cloud`. A name test can't express those — `slapform` is legitimate again
22
- * one level down, and `firebase` lives on as `client.firebase` — so these
23
- * match one EXACT path from the root, with the target NAME read as its TYPE
24
- * (#886) so a `targets.web.*` row fires on every web target a brand runs.
25
- */
26
-
27
- const { TARGETS, CUSTOM_TARGET_TYPE } = require('./schema.js');
28
-
29
- // Every legal target `type`, so a per-type row set covers all of them (#883).
30
- const TARGET_TYPES = [...TARGETS, CUSTOM_TARGET_TYPE];
31
-
32
- /**
33
- * The `translation.exclude` → `translation.include` move (#858): every route
34
- * the brand skipped becomes a negation, in the order it was written, on top
35
- * of `**`. The FRAMEWORK default (`['**', '!blog/**']`) is deliberately not
36
- * what a carrying brand lands on: it was translating its blog, and a
37
- * conversion that changed which pages reach a provider would be a behavior
38
- * change dressed as a rename. Routes are normalized the way the translation
39
- * pass normalizes them (no leading or trailing slash), so `/changelog/` lands
40
- * as `!changelog`.
41
- * @param {*} value - the authored `exclude` list
42
- * @returns {string[]} the `include` list that means the same thing
43
- */
44
- function excludeToInclude(value) {
45
- const routes = Array.isArray(value) ? value : [];
46
-
47
- return ['**', ...routes.map((entry) => `!${String(entry).replace(/^\/+|\/+$/g, '')}`)];
48
- }
49
-
50
- // key name → { replacement, why } (docs/shared/config.md carries the rows)
51
- const RETIRED_KEYS = {
52
- web_manager: {
53
- replacement: 'client',
54
- why: 'it configures @omega.js/client (#1); WebManager is not an OMEGA concept',
55
- },
56
- firebaseConfig: {
57
- replacement: 'cloud',
58
- why: "the provider-discriminated role key: cloud: { provider: 'firebase', config: {…} }",
59
- },
60
- cookieConsent: {
61
- replacement: 'client.consent',
62
- why: 'the banner became a real consent gate (#383) — the block names the DECISION, not the cookie, and its palette/theme/type keys are gone (tokens paint it, the visitor\'s region picks opt-in vs opt-out)',
63
- },
64
-
65
- // The web TARGET is the subdomain (#588). `brand.subdomains` was read by
66
- // ONE thing (the cloud hosting op, which ensured an api.{sub}.{domain} per
67
- // entry) and declared by nothing: no schema rule, no default, never
68
- // materialized. Ian's 2026-09-01 call gave the fact a real home. A NAME test
69
- // by the rule above: `subdomains` exists nowhere else in the schema (the
70
- // legacy searchConsole.subdomains was dropped outright), so the walk catches
71
- // it at every depth, target entries included.
72
- subdomains: {
73
- replacement: 'targets.web',
74
- why: "each subdomain is its own web TARGET (#588/#886): [\"admin\", \"cdn\"] becomes sibling keys beside the main site, `admin: { type: 'web' }, cdn: { type: 'web' }`, where the NAME is the subdomain (https://admin.<brand host>), an entry's own `url` overrides it for a custom host, and the targets share ONE api.<domain>",
75
- },
76
-
77
- // The feature is `connections` now (#788, Ian 2026-09-03): a user connection
78
- // will not always be an OAuth grant — an API key or a bot token is a
79
- // connection too — so the section, the route, the user-record field, the env
80
- // prefix and the brand provider folder all carry the product word, and each
81
- // record names its own kind with `type: 'oauth2'`. A NAME test by the rule
82
- // above: `oauth2` exists nowhere else in the schema.
83
- oauth2: {
84
- replacement: 'connections',
85
- why: "the product concept is a CONNECTION (#788) — the per-provider block is unchanged, the credentials are the CONNECTIONS_<PROVIDER>_CLIENT_ID/_SECRET env pair now, and a brand's own provider lives at targets/backend/src/connections/<name>.js",
86
- },
87
-
88
- // The company block names the parent (#677), so `parent` is retired
89
- // OUTRIGHT: the topology moved to `company: { id }` first, and the last
90
- // meaning it carried, the webhook opt-out, is `company: { webhooks: false }`
91
- // now. A NAME test by the rule above: `parent` exists nowhere else in the
92
- // schema.
93
- parent: {
94
- replacement: 'company.webhooks',
95
- why: "the company block names the parent (#677): the topology is `company: { id: '<parent brand.id>' }` (or 'self'), and the only other thing `parent` ever said, `false` for \"the provider ACCOUNT is shared and its one account-level webhook is owned elsewhere\", is `company: { webhooks: false }`",
96
- },
97
- };
98
-
99
- // exact dotted path → { replacement, why } (docs/shared/config.md carries the rows)
100
- const RETIRED_PATHS = {
101
- slapform: {
102
- replacement: 'forms.providers.slapform',
103
- why: 'config keys name the ROLE, not the vendor (#23)',
104
- },
105
- chatsy: {
106
- replacement: 'inbound.chat.providers.chatsy',
107
- why: 'config keys name the ROLE, not the vendor (#23) — one home for the manager fields and the widget settings',
108
- },
109
- replyify: {
110
- replacement: 'inbound.email.providers.replyify',
111
- why: 'config keys name the ROLE, not the vendor (#23)',
112
- },
113
- cloudflare: {
114
- replacement: 'edge.providers.cloudflare',
115
- why: 'config keys name the ROLE, not the vendor (#23)',
116
- },
117
- recaptcha: {
118
- replacement: 'captcha.providers.recaptcha',
119
- why: 'config keys name the ROLE, not the vendor (#23)',
120
- },
121
- searchConsole: {
122
- replacement: 'search.providers.searchConsole',
123
- why: 'config keys name the ROLE, not the vendor (#23) — `seo` already means the parasite-SEO content feature',
124
- },
125
- gcp: {
126
- replacement: 'cloud',
127
- why: 'one cloud home (#23): gcp.organizationId/billingAccount are now cloud.organizationId/cloud.billingAccount',
128
- },
129
- firebase: {
130
- replacement: 'cloud',
131
- why: 'one cloud home (#23): the provisioning fields are now cloud.shared/supportEmail/apiSubdomain, and projectId lives only at cloud.config.projectId',
132
- },
133
- 'advertising.providers.google-adsense': {
134
- replacement: 'advertising.providers.adsense',
135
- why: 'provider ids drop the vendor prefix and every key is camelCase (#23) — the slots are displaySlot/inArticleSlot/inFeedSlot/multiplexSlot',
136
- },
137
-
138
- // ─── adsense has ONE switch (#527/#628) ───────────────────────────────
139
- // The second gate is what let one config say "stop managing" while the site
140
- // kept serving ads off the same id, so #527 deleted the service's
141
- // `enabled === false` skip and never shipped the `units` half of the same
142
- // proposal. Neither was registered here, so a brand still carrying
143
- // `enabled: false` validated CLEAN — and the account it was meant to leave
144
- // alone started being managed on the next walk, the gate reading exactly
145
- // like it still worked.
146
- 'advertising.providers.adsense.enabled': {
147
- replacement: 'advertising.providers.adsense',
148
- why: 'adsense has ONE switch (#527): `client` presence manages the account, renders the units and writes the ads.txt record together — set `advertising.providers.adsense: false` to opt the provider out, and there is no second gate to disable it with',
149
- },
150
- 'advertising.providers.adsense.units': {
151
- replacement: 'advertising.providers.adsense',
152
- why: 'adsense has ONE switch (#527): the render gate the key proposed was refused — `client` presence is the whole answer, so a managed-but-ad-free brand omits the block and manages the account by hand',
153
- },
154
-
155
- // ─── one provider shape everywhere (#425) ──────────────────────────────
156
- // Every role names its vendors under `providers`, so the flat picks
157
- // (`domain.provider`), the fourth word (`payment.processors`) and the bare
158
- // vendor key (`certificates.apple`) are gone. Each CONVERTED LEAF is its
159
- // own row: a config still carrying the old key would validate clean and
160
- // silently lose the setting, which is exactly what this guard exists for.
161
- 'payment.processors': {
162
- replacement: 'payment.providers',
163
- why: 'one provider shape everywhere (#425) — the block is `providers` in every role, payment included (and the singular word followed in #428: `provider` on a subscription/webhook document, on the payments routes, and in the code)',
164
- },
165
- 'certificates.apple': {
166
- replacement: 'certificates.providers.apple',
167
- why: 'one provider shape everywhere (#425) — no bare vendor keys; Windows signing will sit beside it as certificates.providers.<vendor>',
168
- },
169
- 'domain.provider': {
170
- replacement: 'domain.providers.<registrar>',
171
- why: 'one provider shape everywhere (#425) — the registrar is a KEY under `domain.providers` (namecheap/squarespace); no entry = none chosen and the domain service skips, exactly as a null provider did',
172
- },
173
- 'domain.email.provider': {
174
- replacement: 'domain.email.providers.<provider>',
175
- why: 'one provider shape everywhere (#425) — the mailbox provider is a KEY under `domain.email.providers` (cloudflare/squarespace/privateemail); `domain.email.forwarding` stays role-level',
176
- },
177
- 'translation.provider': {
178
- replacement: 'translation.providers.<name>',
179
- why: 'one provider shape everywhere (#425) — presence picks the engine ({ claude: {} } / { chatgpt: {} }); `translation.model` stays role-level',
180
- },
181
- 'devlog.provider': {
182
- replacement: 'devlog.providers.ghostii',
183
- why: 'one provider shape everywhere (#425) — the writer is a KEY under `devlog.providers`, and its settings moved inside it; `devlog.enabled` stays role-level',
184
- },
185
- 'devlog.lookbackDays': {
186
- replacement: 'devlog.providers.ghostii.lookbackDays',
187
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
188
- },
189
- 'devlog.orgs': {
190
- replacement: 'devlog.providers.ghostii.orgs',
191
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
192
- },
193
- 'devlog.excludeRepos': {
194
- replacement: 'devlog.providers.ghostii.excludeRepos',
195
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
196
- },
197
- 'devlog.excludeCommits': {
198
- replacement: 'devlog.providers.ghostii.excludeCommits',
199
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
200
- },
201
- 'devlog.excludeTopics': {
202
- replacement: 'devlog.providers.ghostii.excludeTopics',
203
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
204
- },
205
- 'devlog.includePrivate': {
206
- replacement: 'devlog.providers.ghostii.includePrivate',
207
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
208
- },
209
- 'devlog.postPath': {
210
- replacement: 'devlog.providers.ghostii.postPath',
211
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
212
- },
213
- 'devlog.destinations': {
214
- replacement: 'devlog.providers.ghostii.destinations',
215
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
216
- },
217
- 'devlog.overrides': {
218
- replacement: 'devlog.providers.ghostii.overrides',
219
- why: 'provider-hung devlog settings live under the provider that reads them (#425)',
220
- },
221
- 'monitoring.provider': {
222
- replacement: 'monitoring.providers.sentry',
223
- why: "one provider shape everywhere (#425) — the monitor is a KEY under `monitoring.providers` (only sentry today); `monitoring.enabled` stays role-level",
224
- },
225
- 'monitoring.org': {
226
- replacement: 'monitoring.providers.sentry.org',
227
- why: 'provider-hung Sentry settings live under the provider that reads them (#425)',
228
- },
229
- 'monitoring.dsn': {
230
- replacement: 'monitoring.providers.sentry.dsn',
231
- why: 'provider-hung Sentry settings live under the provider that reads them (#425): per-surface DSNs are targets.<name>.monitoring.providers.sentry.dsn',
232
- },
233
- 'monitoring.environment': {
234
- replacement: 'monitoring.providers.sentry.environment',
235
- why: 'provider-hung Sentry settings live under the provider that reads them (#425)',
236
- },
237
- 'monitoring.sampleRate': {
238
- replacement: 'monitoring.providers.sentry.sampleRate',
239
- why: 'provider-hung Sentry settings live under the provider that reads them (#425)',
240
- },
241
- 'monitoring.tracesSampleRate': {
242
- replacement: 'monitoring.providers.sentry.tracesSampleRate',
243
- why: 'provider-hung Sentry settings live under the provider that reads them (#425)',
244
- },
245
- 'monitoring.scrubEmail': {
246
- replacement: 'monitoring.providers.sentry.scrubEmail',
247
- why: 'provider-hung Sentry settings live under the provider that reads them (#425)',
248
- },
249
- 'monitoring.attachScreenshot': {
250
- replacement: 'monitoring.providers.sentry.attachScreenshot',
251
- why: 'provider-hung Sentry settings live under the provider that reads them (#425)',
252
- },
253
- 'monitoring.bundlePatterns': {
254
- replacement: 'monitoring.providers.sentry.bundlePatterns',
255
- why: 'provider-hung Sentry settings live under the provider that reads them (#425)',
256
- },
257
- 'marketing.campaigns.provider': {
258
- replacement: 'marketing.campaigns.providers.sendgrid',
259
- why: "one provider shape everywhere (#425) — the email-marketing vendor is a KEY under `marketing.campaigns.providers`; `marketing.campaigns.enabled` stays role-level",
260
- },
261
- 'marketing.campaigns.listId': {
262
- replacement: 'marketing.campaigns.providers.sendgrid.listId',
263
- why: 'the list id is a SendGrid fact and lives under the provider that reads it (#425)',
264
- },
265
- 'marketing.newsletter.provider': {
266
- replacement: 'marketing.newsletter.providers.beehiiv',
267
- why: "one provider shape everywhere (#425) — the newsletter vendor is a KEY under `marketing.newsletter.providers`; `marketing.newsletter.enabled` and `marketing.newsletter.content` stay role-level (content is pipeline config, not Beehiiv config)",
268
- },
269
- 'marketing.newsletter.publicationId': {
270
- replacement: 'marketing.newsletter.providers.beehiiv.publicationId',
271
- why: 'the publication id is a Beehiiv fact and lives under the provider that reads it (#425)',
272
- },
273
- 'blog.provider': {
274
- replacement: 'blog.providers.ghostii',
275
- why: "one provider shape everywhere (#425) — the blog writer is a KEY under `blog.providers`; `blog.enabled` and `blog.content` stay role-level (content is pipeline config)",
276
- },
277
-
278
- // ─── a page's TITLE and DESCRIPTION live in page frontmatter (#607/#564) ──
279
- // The config `meta` section shipped for one wave as the site's default page
280
- // meta, beside the bare `meta:` a page and a layout already wrote — two homes
281
- // for one fact, free to disagree. Ian's 2026-08-26 ruling deleted the copy
282
- // that could not win: the head walk is page `meta:` → layout `meta:` →
283
- // brand.name / brand.description, so a title or description left in
284
- // omega.json5 reaches NOTHING and every page silently falls back to the brand.
285
- //
286
- // `index` is the exception, and #564 (Ian 2026-09-09, the same-name ruling in
287
- // docs/shared/rulings.md) is why the rows are per KEY now rather than on the
288
- // whole block: a site-wide default and the page override of it share ONE name
289
- // at every level, so the site default is `targets.web.meta.index` and the page
290
- // writes `meta.index`. That key is LIVE, and it overlays to the resolved root
291
- // on a web load like every target key, and a row on the whole `meta` object would
292
- // fire on a brand that is simply using it.
293
- //
294
- // Matched at AUTHORED paths, never by key NAME: `analytics.providers.meta` is
295
- // a legitimate key one level down.
296
- 'meta.title': {
297
- replacement: 'brand.name (and the page\'s own frontmatter `meta.title`)',
298
- why: 'a title never exists in two places (#607): page frontmatter `meta:` is the only per-page meta, and the site-wide default is the brand block the head falls back to',
299
- },
300
- 'meta.description': {
301
- replacement: 'brand.description (and the page\'s own frontmatter `meta.description`)',
302
- why: 'a description never exists in two places (#607): page frontmatter `meta:` is the only per-page meta, and the site-wide default is the brand block the head falls back to',
303
- },
304
- 'targets.web.meta.title': {
305
- replacement: 'brand.name (and the page\'s own frontmatter `meta.title`)',
306
- why: 'a title never exists in two places (#607): @omega.js/web reads no site-wide title; a per-page title belongs in that page\'s own frontmatter',
307
- },
308
- 'targets.web.meta.description': {
309
- replacement: 'brand.description (and the page\'s own frontmatter `meta.description`)',
310
- why: 'a description never exists in two places (#607): @omega.js/web reads no site-wide description; a per-page description belongs in that page\'s own frontmatter',
311
- },
312
-
313
- // ─── one index flag, one name at both levels (#564) ────────────────────
314
- // #725 gave the site-wide noindex its own name, `seo.index`, while a page
315
- // said `meta.index`, one decision spelled two ways, which is the defect
316
- // Ian's 2026-09-09 same-name ruling names. The site default is now the same
317
- // key the page writes, under the target that reads it.
318
- 'seo.index': {
319
- replacement: 'targets.web.meta.index',
320
- why: 'a global value and its specific override share ONE name (#564, Ian 2026-09-09): the site-wide default is `targets.web.meta.index` and a page overrides it with `meta.index` in its own frontmatter; `seo` keeps `enabled` and `github.content`',
321
- },
322
-
323
- // ─── one home for the download/extension links (#610) ──────────────────
324
- // The hand-written page maps survived beside the derivation #85/#124 added,
325
- // so explicit config could silently override the release the desktop target
326
- // actually ships. Matched at their AUTHORED path: `targets.web` is where the
327
- // converter wrote them and where every carrying brand still has them, and
328
- // the merged targets map rides every resolved config, so one row fires once
329
- // for every target load (a root `download` on a web load is the same key
330
- // overlaid, not a second mistake).
331
- 'targets.web.download': {
332
- replacement: 'targets.desktop.releases',
333
- why: 'two homes for one fact (#610) — the /download page and its shortlinks derive from the desktop target\'s releases block, curated onto site.targets.desktop.releasesUrl; a hand-written map could point at a release that does not exist',
334
- },
335
- 'targets.web.extension': {
336
- replacement: 'targets.extension.listings',
337
- why: 'two homes for one fact (#610) — the /extension page and its shortlinks read the extension target\'s store listings, curated onto site.targets.extension.listings',
338
- },
339
-
340
- // ─── the four schema-less web sections (#850) ─────────────────────────
341
- // Everything the build processes has a schema home (Ian 2026-09-09). These
342
- // four were the exception: presentation blocks the converter
343
- // wrote under `targets.web`, which @omega.js/web carried in a PRIVATE list
344
- // (`WEB_ONLY_SECTIONS`) purely to let its own `config:` guard pass them.
345
- // The list is gone, so each one is a registered path instead of a key that
346
- // validates clean and reaches nothing. Matched at their AUTHORED path, the
347
- // one place a carrying brand has them (a root spelling on a web load is the
348
- // same key overlaid, exactly as with the #610 pair above).
349
- 'targets.web.favicon': {
350
- replacement: 'nothing for the path; brand.images.favicon for the source image',
351
- why: 'the favicon set is MINTED from the brand images (#850): the manager assets service mints it and the web build bridges it to /assets/images/favicon, so a path override pointed the whole site at an unminted folder; theme-color comes from `brand.color` now, the one place a brand states its hex',
352
- },
353
- 'targets.web.manifest': {
354
- replacement: 'nothing: the minted set ships site.webmanifest',
355
- why: 'no reader anywhere in @omega.js/web (#850): the web app manifest that ships is the minted set\'s own site.webmanifest, so every key under this block was a value nothing consulted',
356
- },
357
- 'targets.web.icons': {
358
- replacement: 'the `icon` on the link itself, as Font Awesome classes',
359
- why: 'one icon mechanism (#619): a footer link carries its own `fa-*` classes (`icon: \'fa-brands fa-github\'`), so the name-to-markup map is gone; the legacy block only ever held a `style`, which the map lookup could never resolve',
360
- },
361
- 'targets.web.currency': {
362
- replacement: 'payment.currency',
363
- why: 'two homes for one fact (#850): the price currency belongs to the payment section every other surface reads it from, and the pricing JSON-LD reads it there',
364
- },
365
-
366
- // ─── redirects are not web config (#466) ──────────────────────────────
367
- // The block shipped for one wave (0.45.0) and was withdrawn: static hosting
368
- // has no server, so the map could only be answered CLIENT-side off the built
369
- // 404 page — a search engine saw a 404 that redirects, never a move. A
370
- // TEMPLATED redirect genuinely needs edge computing and is a Cloudflare
371
- // redirect rule; an enumerable one is a redirect PAGE. Matched at its
372
- // AUTHORED path, the one place a carrying brand has it.
373
- 'targets.web.redirects': {
374
- replacement: 'edge.providers.cloudflare.rules.redirect',
375
- why: 'redirects are not web config (#466) — a TEMPLATED redirect (/c/:id → /code?id=:id) is a Cloudflare redirect rule the edge service reconciles, and a redirect whose URLs can be enumerated is a PAGE on the `modules/utilities/redirect` layout with `redirect.url` in its frontmatter',
376
- },
377
-
378
- // ─── the bundler has no consumer override (#737) ──────────────────────
379
- // Desktop moved off webpack and the externals knob went with it: esbuild's
380
- // externals set is the framework's own native-module list plus whatever the
381
- // consumer's package.json declares from it, resolved at build time. Nothing
382
- // reads the key any more — and it sits inside the `targets` namespace the
383
- // undeclared-key warning EXEMPTS, so without a row here a brand still
384
- // carrying it validates completely clean and silently loses the setting.
385
- // Matched at its AUTHORED path, the one place a carrying brand has it.
386
- 'targets.desktop.em.webpack.externals': {
387
- replacement: 'nothing — the externals set is framework-owned',
388
- why: "the desktop bundler is esbuild (#737) and there is no consumer-facing override key: the externals set is the framework's native-module list (`nativeExternals` in @omega.js/desktop's src/gulp/tasks/bundle.js) plus what the consumer's own package.json declares from it, so a genuinely native module the list misses is raised upstream and every brand gets the fix",
389
- },
390
-
391
- // ─── one features catalog, one values map (#647) ───────────────────────
392
- // A metered feature used to be spelled twice on every product — a number in
393
- // `limits` and a display row in the `features` ARRAY — and its PACING was a
394
- // product-wide `rateLimit` that no single feature could opt out of. The
395
- // catalog defines each feature once (name, icon, definition, and the `usage`
396
- // block that meters it) and a product names only its value, so a limit and
397
- // the row that renders it can no longer disagree. Matched at their AUTHORED
398
- // paths: `limits` is a legitimate word elsewhere, so this is not a name test.
399
- 'payment.products.limits': {
400
- replacement: 'payment.products[].features',
401
- why: 'a product names one VALUE per feature (#647) — `limits: { saves: 100 }` becomes `features: { saves: 100 }`, and the feature itself (name, icon, definition, pacing, mirrors) is defined once in the top-level `features` catalog',
402
- },
403
- 'payment.products.rateLimit': {
404
- replacement: 'features.<id>.usage.pace',
405
- why: 'pacing is per FEATURE now (#647) — day pacing is the default on every counted feature, and `usage: { pace: false }` on the catalog entry is the opt-out the product-wide `rateLimit: "monthly"` used to be',
406
- },
407
-
408
- // ─── one releases repo, no mirror (#620/#799) ──────────────────────────
409
- // The `download-server` mirror existed to give marketing a fixed filename,
410
- // which the versionless artifact names (#620) made free: the site links the
411
- // releases repo directly and reads nothing from the mirror. #799 deleted the
412
- // lane (gulp/mirror-downloads, the finalize-release mirror step, the repo
413
- // provisioning), so a brand still carrying the block gets a second repo
414
- // provisioned and nothing published to it. Matched at their AUTHORED paths,
415
- // one row per key: `downloads` is a legitimate word elsewhere (the curated
416
- // site.targets.desktop.downloads map is the derived direct-download links),
417
- // so this is not a name test.
418
- 'targets.desktop.downloads.enabled': {
419
- replacement: 'targets.desktop.releases',
420
- why: 'one public releases repo per brand (#620/#799): the fixed-name mirror is gone, and the versionless assets on the releases repo ARE the permanent download links',
421
- },
422
- 'targets.desktop.downloads.owner': {
423
- replacement: 'targets.desktop.releases.owner',
424
- why: 'one public releases repo per brand (#620/#799): there is no second repo to own, and the releases owner defaults to the brand repo owner',
425
- },
426
- 'targets.desktop.downloads.repo': {
427
- replacement: 'targets.desktop.releases.repo',
428
- why: 'one public releases repo per brand (#620/#799): the release artifacts and the marketing downloads are the same assets in `<brand.id>-releases`',
429
- },
430
- 'targets.desktop.downloads.tag': {
431
- replacement: 'nothing: the versionless assets live on the `v<x.y.z>` release',
432
- why: 'one public releases repo per brand (#620/#799): a stable mirror tag is what `/releases/latest/download/<asset>` replaced, so no tag is configured anywhere',
433
- },
434
-
435
- // ─── one repo block, and no repo name anywhere (#883) ──────────────────
436
- // A brand's repo hosting used to be spelled in four places: the
437
- // `repo.providers.github` block, a separate top-level `github` identity, a
438
- // `targets.<name>.github.repo` override, and the desktop releases
439
- // owner/repo. One block says it now (`repo: { provider, org }`), every repo
440
- // name derives from `<brand.id>-<role>`, and visibility is the brand root
441
- // package.json's `private` field. Matched at their AUTHORED paths, one row
442
- // per LEAF: each of these validated clean under the new shape and silently
443
- // addressed a repo nothing publishes to.
444
- 'repo.providers.github.org': {
445
- replacement: 'repo.org',
446
- why: 'ONE repo block (#883): `repo: { provider: \'github\', org }` is the whole declaration, and the org owns every repo the brand derives',
447
- },
448
- 'repo.providers.github.repo': {
449
- replacement: 'nothing: the source repo IS `<brand.id>-omega`',
450
- why: 'no repo NAME is configurable anywhere (#883): a repo name that must differ is a brand id that must differ, so rename the GitHub repo to `<brand.id>-omega` instead',
451
- },
452
- 'repo.providers.github.private': {
453
- replacement: 'the brand root package.json `private` field',
454
- why: 'visibility has ONE statement (#883): `private: true` (or absent) is a private brand, `false` a public one, and the manage walk reconciles the repo to it in both directions',
455
- },
456
- 'repo.providers.github.shared': {
457
- replacement: 'nothing: an org may host many brands, and no brand rewrites an org profile',
458
- why: 'the org-profile reconcile is gone (#883), so there is no shared-org exception left to declare',
459
- },
460
- 'repo.providers.github.enabled': {
461
- replacement: 'the presence of the `repo` block',
462
- why: 'presence is the switch (#883), exactly as a target key\'s presence enables that target: a brand that hosts its source somewhere the manager does not touch omits the block',
463
- },
464
- 'github.user': {
465
- replacement: 'nothing: the org is `repo.org`',
466
- why: 'the separate GitHub identity block is gone (#883): nothing read `user`, and every repo address derives from `repo.org` plus `brand.id`',
467
- },
468
- 'github.website': {
469
- replacement: 'nothing: a web target\'s site repo is `<brand.id>-<target name>`',
470
- why: 'the website repo derives now (#883): each GitHub-hosted web target publishes its built site to its own `<brand.id>-<name>` repo, Pages serving it at the target\'s url',
471
- },
472
- // ─── the company is ONE key, outside `brand` (#677) ────────────────────
473
- // `brand.company` was a typed DISPLAY NAME (Ian 2026-09-12: "brand.company
474
- // hardcoded was a workaround"): the parent's name is the parent's to state,
475
- // so it resolves now. Registered at its authored path, because `company` is
476
- // a legitimate key one level up and at the top level.
477
- 'brand.company': {
478
- replacement: 'company.name (resolved from `company: { id }`)',
479
- why: "the company is ONE top-level key now (#677): type `company: { id: '<parent brand.id>' }` (or 'self') and the loader fills company.name/url/images from the parent's own config, so no brand restates its parent's facts",
480
- },
481
- 'brand.images.companyWordmark': {
482
- replacement: 'company.images.wordmark (resolved from `company: { id }`)',
483
- why: "the parent's wordmark is the parent's own `brand.images.wordmark` (#677), resolved through `company: { id }` for every sub-brand instead of pasted into each one",
484
- },
485
-
486
- // ─── the ONE platform vocabulary (#867) ───────────────────────────────
487
- // The client's words everywhere (`mac`, `windows`, `linux`), and what a
488
- // target ships is DECLARED per format. Both of these validated clean while
489
- // nothing read them: a brand still saying `win` got @omega.js/desktop's
490
- // default Windows settings and its own were ignored, and a brand still
491
- // saying `snap.enabled: true` published no snap at all. Registered at both
492
- // shapes a brand can write them in: inside the target entry (the brand file)
493
- // and at the top level (a target's own config/omega.json5).
494
- 'platforms.win': {
495
- replacement: 'platforms.windows (with its formats inside: platforms.windows.formats.nsis)',
496
- why: "ONE platform vocabulary, the client's (#867): run `npx omega manage --migration=platform-names --execute` at the brand root to rewrite it (it also renames config/icons/macos/ to config/icons/mac/). Run the migration BEFORE `omega migrate`, which deletes a retired key rather than moving it",
497
- },
498
- 'targets.desktop.platforms.win': {
499
- replacement: 'targets.desktop.platforms.windows (with its formats inside: platforms.windows.formats.nsis)',
500
- why: "ONE platform vocabulary, the client's (#867): run `npx omega manage --migration=platform-names --execute` at the brand root to rewrite it (it also renames config/icons/macos/ to config/icons/mac/). Run the migration BEFORE `omega migrate`, which deletes a retired key rather than moving it",
501
- },
502
- 'platforms.linux.snap': {
503
- replacement: 'platforms.linux.formats.snap (presence IS the switch, so the `enabled` flag is gone)',
504
- why: 'what a target ships is one declaration now (#867): every platform and format defaults ON and `platforms.linux.formats.snap: false` is the only way to drop the snap. Its settings (channels, confinement, grade, autoStart) move inside the format. `npx omega manage --migration=platform-names --execute` performs the move',
505
- },
506
- 'targets.desktop.platforms.linux.snap': {
507
- replacement: 'targets.desktop.platforms.linux.formats.snap (presence IS the switch, so the `enabled` flag is gone)',
508
- why: 'what a target ships is one declaration now (#867): every platform and format defaults ON and `platforms.linux.formats.snap: false` is the only way to drop the snap. Its settings (channels, confinement, grade, autoStart) move inside the format. `npx omega manage --migration=platform-names --execute` performs the move',
509
- },
510
-
511
- // ─── the translation list says what to TRANSLATE (#858) ───────────────
512
- // Ian 2026-09-13: an exclude list defaulted to "translate everything", so a
513
- // brand that never thought about it paid a provider for its whole blog. The
514
- // list is `translation.include` now: globs with `!` negation, defaulting to
515
- // ['**', '!blog/**'] in the DEFAULTS layer, and the page half is the same
516
- // key one level down (`translation.include: true`/`false` in frontmatter),
517
- // which is Ian's 2026-09-09 same-name ruling. The row carries a `convert`,
518
- // so `omega migrate` MOVES the setting instead of only deleting it: each
519
- // excluded route becomes a negation on top of the default. Registered at its
520
- // authored path AND at the `targets.web` overlay, the two places a brand can
521
- // write it; `translation` is a legitimate key name at both.
522
- 'translation.exclude': {
523
- replacement: 'translation.include',
524
- why: "the route list says what to TRANSLATE now (#858, Ian 2026-09-13): globs with `!` negation, read in .gitignore order, defaulting to ['**', '!blog/**'], so `exclude: ['docs']` becomes `include: ['**', '!docs']`, and a page overrides it for itself with `translation.include: true`/`false` in its own frontmatter. `omega migrate` performs the move",
525
- convert: excludeToInclude,
526
- },
527
- 'targets.web.translation.exclude': {
528
- replacement: 'targets.web.translation.include',
529
- why: "the route list says what to TRANSLATE now (#858, Ian 2026-09-13): globs with `!` negation, read in .gitignore order, defaulting to ['**', '!blog/**'], so `exclude: ['docs']` becomes `include: ['**', '!docs']`, and a page overrides it for itself with `translation.include: true`/`false` in its own frontmatter. `omega migrate` performs the move",
530
- convert: excludeToInclude,
531
- },
532
-
533
- 'targets.desktop.releases.owner': {
534
- replacement: 'nothing: the releases repo is `<brand.id>-releases` under `repo.org`',
535
- why: 'one public releases repo per brand (#883), with no override: `releases: {}` stays the presence switch for the site\'s download links',
536
- },
537
- 'targets.desktop.releases.repo': {
538
- replacement: 'nothing: the releases repo is `<brand.id>-releases` under `repo.org`',
539
- why: 'one public releases repo per brand (#883), with no override: `releases: {}` stays the presence switch for the site\'s download links',
540
- },
541
- };
542
-
543
- // The per-target `github` override, one row per TYPE (#883). The type-row rule
544
- // above (shapePath reads a target's NAME as its TYPE) makes each row fire on
545
- // every target of that type a brand runs, and enumerating TARGET_TYPES is what
546
- // keeps a type nobody thought of from slipping through: the backend's CMS
547
- // content repo is the override that existed, but the key validated clean on any
548
- // target and pointed the commits at a repo nothing else addressed.
549
- for (const type of TARGET_TYPES) {
550
- RETIRED_PATHS[`targets.${type}.github.repo`] = {
551
- replacement: 'nothing: every repo the brand owns is `<brand.id>-<role>` under `repo.org`',
552
- why: 'no repo NAME is configurable anywhere (#883): the CMS commits to the SOURCE repo `<brand.id>-omega`, so a content repo of its own is a brand of its own',
553
- };
554
- }
555
-
556
- // The walked path is not always the SHAPE a RETIRED_PATHS row names, two ways:
557
- // a value inside an array carries its position (`redirects.0.from`), and a
558
- // target carries its NAME while the rows name its TYPE (#886). Both are
559
- // normalized for MATCHING only: the REPORTED path stays the real one, which is
560
- // where the author finds the key.
561
- //
562
- // The name normalization is what makes a `targets.web.*` row fire on EVERY web
563
- // target a brand runs (`targets.community.meta.title` when community is
564
- // `type: 'web'`), which is the shape a brand with two sites has (#732, #886).
565
- function shapePath(keyPath, targetTypes) {
566
- const parts = keyPath.split('.').filter((segment) => !/^\d+$/.test(segment));
567
-
568
- if (parts[0] === 'targets' && parts.length > 1 && targetTypes[parts[1]]) {
569
- parts[1] = targetTypes[parts[1]];
570
- }
571
-
572
- return parts.join('.');
573
- }
574
-
575
- /**
576
- * Every declared target's name → type, read off the config being walked. A
577
- * sub-tree with no `targets` map (or entries with no `type`) yields {}, so the
578
- * rows then match by the key as written.
579
- * @param {object} object - Parsed config (or any sub-tree of one).
580
- * @returns {object} name → type.
581
- */
582
- function targetTypes(object) {
583
- const targets = object && object.targets;
584
- if (!targets || typeof targets !== 'object' || Array.isArray(targets)) return {};
585
-
586
- const types = {};
587
- Object.keys(targets).forEach((name) => {
588
- const entry = targets[name];
589
- if (entry && typeof entry === 'object' && !Array.isArray(entry) && typeof entry.type === 'string') {
590
- types[name] = entry.type;
591
- }
592
- });
593
-
594
- return types;
595
- }
596
-
597
- function walk(node, path, found, types) {
598
- if (Array.isArray(node)) {
599
- node.forEach((item, index) => walk(item, path ? `${path}.${index}` : String(index), found, types));
600
- return;
601
- }
602
-
603
- if (typeof node !== 'object' || node === null) {
604
- return;
605
- }
606
-
607
- Object.keys(node).forEach((key) => {
608
- const keyPath = path ? `${path}.${key}` : key;
609
-
610
- if (RETIRED_KEYS[key]) {
611
- found.push({ path: keyPath, key, ...RETIRED_KEYS[key] });
612
- }
613
-
614
- const shape = shapePath(keyPath, types);
615
-
616
- if (RETIRED_PATHS[shape]) {
617
- found.push({ path: keyPath, key, ...RETIRED_PATHS[shape] });
618
- }
619
-
620
- walk(node[key], keyPath, found, types);
621
- });
622
- }
623
-
624
- /**
625
- * Recursively find retired config keys.
626
- * @param {object} object - Parsed config (or any sub-tree of one).
627
- * @returns {Array<{ path: string, key: string, replacement: string, why: string }>}
628
- */
629
- function findRetiredKeys(object) {
630
- const found = [];
631
- walk(object, '', found, targetTypes(object));
632
- return found;
633
- }
634
-
635
- module.exports = { findRetiredKeys, RETIRED_KEYS, RETIRED_PATHS };