@omega.js/desktop 0.52.0 → 0.53.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 (273) hide show
  1. package/README.md +17 -12
  2. package/dist/assets/css/core/_initialize.scss +1 -1
  3. package/dist/assets/js/core/app-shell.js +14 -15
  4. package/dist/assets/themes/_template/_theme.js +6 -6
  5. package/dist/assets/themes/base/_includes/global/sections/account.html +4 -4
  6. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +5 -5
  7. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +3 -3
  8. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +1 -1
  9. package/dist/assets/themes/base/_layouts/frontend/pages/blog/tags/tag.html +1 -1
  10. package/dist/assets/themes/base/_layouts/frontend/pages/payment/confirmation.html +1 -1
  11. package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.js +2 -3
  12. package/dist/assets/themes/base/_sections/verts/unit/section.html +1 -1
  13. package/dist/assets/themes/base/_sections/verts/unit/section.js +3 -4
  14. package/dist/assets/themes/base/_theme.js +4 -3
  15. package/dist/assets/themes/bootstrap/_theme.js +2 -2
  16. package/dist/assets/themes/bootstrap/overrides/_links.scss +1 -1
  17. package/dist/assets/themes/classy/_theme.js +8 -6
  18. package/dist/assets/themes/classy/css/marketing/_sections.scss +1 -2
  19. package/dist/assets/themes/classy/js/hero-demo-form.js +3 -2
  20. package/dist/assets/themes/neobrutalism/_theme.js +5 -5
  21. package/dist/assets/themes/neobrutalism/js/pages/test/libraries/layers/index.js +1 -1
  22. package/dist/assets/themes/newsflash/_theme.js +6 -6
  23. package/dist/assets/themes/newsflash/js/pages/test/libraries/layers/index.js +1 -1
  24. package/dist/build.js +87 -111
  25. package/dist/cli.js +1 -1
  26. package/dist/commands/build.js +2 -2
  27. package/dist/commands/cdp/capture.js +2 -2
  28. package/dist/commands/cdp/quit.js +2 -2
  29. package/dist/commands/cdp/relaunch.js +2 -2
  30. package/dist/commands/cdp/theme.js +1 -1
  31. package/dist/commands/clean.js +2 -2
  32. package/dist/commands/deploy.js +4 -4
  33. package/dist/commands/finalize-release.js +4 -4
  34. package/dist/commands/install.js +3 -3
  35. package/dist/commands/launch.js +2 -2
  36. package/dist/commands/lib/deploy-precheck.js +3 -3
  37. package/dist/commands/lib/ensure-target.js +6 -6
  38. package/dist/commands/package.js +2 -2
  39. package/dist/commands/publish.js +2 -2
  40. package/dist/commands/release.js +3 -3
  41. package/dist/commands/runner.js +11 -11
  42. package/dist/commands/sign-windows.js +5 -5
  43. package/dist/commands/test.js +4 -4
  44. package/dist/commands/update.js +2 -2
  45. package/dist/commands/validate-certs.js +4 -4
  46. package/dist/commands/version.js +3 -3
  47. package/dist/defaults/AGENTS.md +17 -8
  48. package/dist/defaults/config/omega.json5 +7 -7
  49. package/dist/defaults/hooks/build/post.js +1 -1
  50. package/dist/defaults/hooks/build/pre.js +1 -1
  51. package/dist/defaults/hooks/deploy/pre.js +1 -1
  52. package/dist/defaults/hooks/release/post.js +1 -1
  53. package/dist/defaults/hooks/release/pre.js +1 -1
  54. package/dist/defaults/src/assets/js/components/about/index.js +3 -5
  55. package/dist/defaults/src/assets/js/components/main/index.js +3 -5
  56. package/dist/defaults/src/assets/js/components/settings/index.js +3 -5
  57. package/dist/defaults/src/integrations/context-menu/index.js +2 -2
  58. package/dist/defaults/src/integrations/menu/index.js +3 -3
  59. package/dist/defaults/src/integrations/tray/index.js +3 -3
  60. package/dist/defaults/src/main.js +3 -5
  61. package/dist/defaults/src/preload.js +3 -5
  62. package/dist/defaults/test/README.md +2 -2
  63. package/dist/gulp/main.js +9 -10
  64. package/dist/gulp/tasks/audit.js +7 -7
  65. package/dist/gulp/tasks/build-config.js +8 -8
  66. package/dist/gulp/tasks/bundle.js +16 -16
  67. package/dist/gulp/tasks/defaults.js +3 -3
  68. package/dist/gulp/tasks/distribute.js +2 -2
  69. package/dist/gulp/tasks/html.js +9 -9
  70. package/dist/gulp/tasks/package-quick.js +3 -3
  71. package/dist/gulp/tasks/package.js +3 -3
  72. package/dist/gulp/tasks/release.js +3 -3
  73. package/dist/gulp/tasks/sass.js +6 -6
  74. package/dist/gulp/tasks/serve.js +4 -4
  75. package/dist/hooks/notarize-artifacts.js +1 -1
  76. package/dist/hooks/notarize.js +1 -1
  77. package/dist/index.js +5 -8
  78. package/dist/lib/_environment-mixin.js +50 -0
  79. package/dist/lib/_lifecycle-mixin.js +45 -0
  80. package/dist/lib/analytics.js +33 -35
  81. package/dist/lib/app-state.js +14 -14
  82. package/dist/lib/auth-flow.js +18 -18
  83. package/dist/lib/auth-persistence.js +12 -12
  84. package/dist/lib/auth.js +421 -0
  85. package/dist/lib/auto-updater.js +49 -49
  86. package/dist/lib/context-menu.js +13 -13
  87. package/dist/lib/context.js +19 -19
  88. package/dist/lib/deep-link.js +34 -34
  89. package/dist/lib/fontawesome.js +5 -5
  90. package/dist/lib/ipc.js +4 -4
  91. package/dist/lib/menu.js +25 -25
  92. package/dist/lib/protocol.js +5 -5
  93. package/dist/lib/remote-config.js +22 -22
  94. package/dist/lib/remote-scripts.js +21 -21
  95. package/dist/lib/restart-manager/index.js +28 -28
  96. package/dist/lib/sign-helpers/exec-with-limit.js +1 -1
  97. package/dist/lib/sign-helpers/sign-events.js +1 -1
  98. package/dist/lib/startup.js +18 -13
  99. package/dist/lib/storage.js +10 -10
  100. package/dist/lib/templating.js +16 -16
  101. package/dist/lib/theme.js +10 -10
  102. package/dist/lib/tray.js +27 -27
  103. package/dist/lib/usage.js +11 -11
  104. package/dist/lib/window-manager.js +26 -26
  105. package/dist/main.js +398 -483
  106. package/dist/preload.js +236 -176
  107. package/dist/renderer.js +417 -391
  108. package/dist/test/fixtures/consumer-app/config/omega.json5 +1 -1
  109. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +8 -11
  110. package/dist/test/fixtures/consumer-app/src/main.js +5 -7
  111. package/dist/test/fixtures/consumer-app/src/preload.js +2 -2
  112. package/dist/test/harness/boot-entry.js +22 -20
  113. package/dist/test/harness/main-entry.js +31 -30
  114. package/dist/test/harness/renderer-entry.js +5 -5
  115. package/dist/test/harness/renderer-preload.js +137 -141
  116. package/dist/test/index.js +10 -10
  117. package/dist/test/runner.js +2 -2
  118. package/dist/test/runners/boot.js +7 -6
  119. package/dist/test/runners/electron.js +3 -2
  120. package/dist/test/runners/render-event.js +2 -2
  121. package/dist/test/suites/boot/consumer-app-boots.test.js +57 -26
  122. package/dist/test/suites/boot/restart-manager.test.js +2 -2
  123. package/dist/test/suites/boot/storage-bundled.test.js +5 -5
  124. package/dist/test/suites/boot/theme.test.js +13 -13
  125. package/dist/test/suites/build/audit.test.js +1 -1
  126. package/dist/test/suites/build/auth-persistence-resolve.test.js +6 -6
  127. package/dist/test/suites/build/boot-fixture.test.js +2 -2
  128. package/dist/test/suites/build/boot-runner-timeout.test.js +6 -5
  129. package/dist/test/suites/build/brand-scss.test.js +1 -1
  130. package/dist/test/suites/build/build-json-bake.test.js +1 -1
  131. package/dist/test/suites/build/cli.test.js +2 -2
  132. package/dist/test/suites/build/config-schema.test.js +5 -5
  133. package/dist/test/suites/build/defaults-scaffold.test.js +2 -2
  134. package/dist/test/suites/build/deploy-hook.test.js +3 -3
  135. package/dist/test/suites/build/ensure-target.test.js +2 -2
  136. package/dist/test/suites/build/env-delivery.test.js +2 -2
  137. package/dist/test/suites/build/esm-only-dependency.test.js +2 -2
  138. package/dist/test/suites/build/exports.test.js +9 -8
  139. package/dist/test/suites/build/get-config.test.js +4 -4
  140. package/dist/test/suites/build/manifest-deps.test.js +1 -1
  141. package/dist/test/suites/build/merge-line-files.test.js +1 -1
  142. package/dist/test/suites/build/omega-shell.test.js +34 -2
  143. package/dist/test/suites/build/omega.test.js +350 -0
  144. package/dist/test/suites/build/renderer-auth-bridge.test.js +211 -78
  145. package/dist/test/suites/build/runner.test.js +2 -2
  146. package/dist/test/suites/build/sentry.test.js +2 -2
  147. package/dist/test/suites/build/sign-windows-e2e.test.js +2 -2
  148. package/dist/test/suites/build/templating.test.js +3 -3
  149. package/dist/test/suites/build/test-stealth.test.js +7 -9
  150. package/dist/test/suites/build/url-helpers.test.js +55 -56
  151. package/dist/test/suites/build/validate-config.test.js +2 -2
  152. package/dist/test/suites/build/wave5-pins.test.js +2 -2
  153. package/dist/test/suites/main/analytics.test.js +59 -59
  154. package/dist/test/suites/main/app-state.test.js +66 -66
  155. package/dist/test/suites/main/auth-flow.test.js +41 -41
  156. package/dist/test/suites/main/auth-persistence.test.js +54 -43
  157. package/dist/test/suites/main/{client-bridge.integration.test.js → auth.integration.test.js} +10 -9
  158. package/dist/test/suites/main/auth.test.js +336 -0
  159. package/dist/test/suites/main/auto-updater.test.js +134 -134
  160. package/dist/test/suites/main/boot-sequence.test.js +37 -49
  161. package/dist/test/suites/main/context-menu.test.js +51 -50
  162. package/dist/test/suites/main/context.test.js +25 -25
  163. package/dist/test/suites/main/deep-link.test.js +74 -74
  164. package/dist/test/suites/main/fontawesome.test.js +27 -27
  165. package/dist/test/suites/main/ipc.test.js +35 -35
  166. package/dist/test/suites/main/menu.test.js +101 -100
  167. package/dist/test/suites/main/protocol.test.js +19 -19
  168. package/dist/test/suites/main/remote-config.test.js +63 -63
  169. package/dist/test/suites/main/remote-scripts.test.js +103 -103
  170. package/dist/test/suites/main/request.test.js +71 -0
  171. package/dist/test/suites/main/restart-manager.test.js +33 -33
  172. package/dist/test/suites/main/startup-paths-and-ua.test.js +4 -4
  173. package/dist/test/suites/main/startup.test.js +26 -26
  174. package/dist/test/suites/main/stealth-window.test.js +1 -1
  175. package/dist/test/suites/main/storage.test.js +25 -25
  176. package/dist/test/suites/main/theme.test.js +34 -34
  177. package/dist/test/suites/main/tray.test.js +79 -79
  178. package/dist/test/suites/main/url-helpers.test.js +133 -133
  179. package/dist/test/suites/main/usage.test.js +25 -25
  180. package/dist/test/suites/main/window-bounds.test.js +27 -27
  181. package/dist/test/suites/main/window-manager.test.js +44 -44
  182. package/dist/test/suites/renderer/analytics-bridge.test.js +5 -5
  183. package/dist/test/suites/renderer/cross-context-helpers.test.js +37 -31
  184. package/dist/test/suites/renderer/round-trip.test.js +3 -3
  185. package/dist/test/suites/renderer/tooltips.test.js +15 -15
  186. package/dist/test/suites/renderer/{window-em-surface.test.js → window-desktop-surface.test.js} +27 -7
  187. package/dist/test/utils/extended-mode-warning.js +1 -1
  188. package/dist/utils/boot-harness.js +56 -0
  189. package/dist/utils/mode-helpers.js +2 -15
  190. package/dist/utils/ship-keys.js +3 -3
  191. package/dist/utils/signing-status.js +51 -0
  192. package/dist/utils/test-events.js +7 -0
  193. package/dist/utils/test-stealth.js +6 -6
  194. package/dist/utils/url-helpers.js +52 -42
  195. package/dist/utils/user-agent.js +44 -0
  196. package/dist/vendor/account/engine.js +3 -3
  197. package/dist/vendor/account/index.js +14 -45
  198. package/dist/vendor/account/resolve-account.js +44 -0
  199. package/dist/vendor/account/schema.js +1 -1
  200. package/dist/vendor/account/user.js +99 -0
  201. package/dist/vendor/config/client-config.js +1 -1
  202. package/dist/vendor/config/environment.js +11 -30
  203. package/dist/vendor/config/index.js +8 -11
  204. package/dist/vendor/config/load.js +1 -2
  205. package/dist/vendor/config/platforms.js +1 -1
  206. package/dist/vendor/config/retired-keys.js +2 -2
  207. package/dist/vendor/config/schema.js +5 -8
  208. package/dist/vendor/config/site-global.js +2 -3
  209. package/dist/vendor/config/validate.js +1 -2
  210. package/dist/vendor/config/winback.js +1 -1
  211. package/dist/vendor/devkit/actions-secrets.js +1 -1
  212. package/dist/vendor/devkit/attach-log-file.js +1 -1
  213. package/dist/vendor/devkit/build-json.js +1 -1
  214. package/dist/vendor/devkit/cli-router.js +3 -4
  215. package/dist/vendor/devkit/defaults-engine.js +9 -11
  216. package/dist/vendor/devkit/local.js +2 -0
  217. package/dist/vendor/devkit/merge-line-files.js +2 -3
  218. package/dist/vendor/devkit/test/runner-core.js +6 -6
  219. package/dist/vendor/monitoring/env.js +2 -2
  220. package/dist/vendor/monitoring/index.js +1 -1
  221. package/dist/vendor/monitoring/main.js +1 -1
  222. package/dist/vendor/monitoring/preload.js +1 -1
  223. package/dist/vendor/monitoring/renderer.js +1 -1
  224. package/docs/analytics.md +8 -8
  225. package/docs/app-state.md +19 -19
  226. package/docs/audit.md +4 -4
  227. package/docs/{client-bridge.md → auth.md} +88 -73
  228. package/docs/auto-updater.md +8 -8
  229. package/docs/boot-sequence.md +11 -6
  230. package/docs/build-system.md +1 -1
  231. package/docs/cdp-debugging.md +1 -1
  232. package/docs/common-mistakes.md +7 -7
  233. package/docs/config-schema.md +1 -1
  234. package/docs/context-menu.md +13 -13
  235. package/docs/context.md +11 -11
  236. package/docs/css.md +3 -9
  237. package/docs/deep-link.md +25 -25
  238. package/docs/environment-detection.md +16 -16
  239. package/docs/fontawesome.md +7 -5
  240. package/docs/hooks.md +8 -8
  241. package/docs/index.md +38 -27
  242. package/docs/ipc.md +10 -10
  243. package/docs/lib-modules.md +9 -9
  244. package/docs/logging.md +12 -14
  245. package/docs/menu.md +18 -18
  246. package/docs/remote-config.md +9 -9
  247. package/docs/remote-scripts.md +12 -12
  248. package/docs/restart-manager.md +8 -8
  249. package/docs/sentry.md +4 -4
  250. package/docs/shared/analytics.md +1 -1
  251. package/docs/shared/breaking-changes.md +79 -13
  252. package/docs/shared/config.md +17 -21
  253. package/docs/shared/logging.md +1 -1
  254. package/docs/shared/monitoring.md +5 -5
  255. package/docs/shared/testing.md +2 -2
  256. package/docs/shared/theming.md +1 -1
  257. package/docs/shared/translation.md +19 -10
  258. package/docs/startup.md +16 -16
  259. package/docs/storage.md +11 -11
  260. package/docs/templating.md +3 -3
  261. package/docs/test-boot-layer.md +12 -12
  262. package/docs/test-framework.md +17 -17
  263. package/docs/themes.md +7 -7
  264. package/docs/tooltips.md +2 -2
  265. package/docs/tray.md +31 -31
  266. package/docs/usage.md +7 -7
  267. package/docs/verts.md +1 -1
  268. package/docs/windows.md +22 -22
  269. package/package.json +3 -4
  270. package/dist/lib/client-bridge.js +0 -374
  271. package/dist/lib/logger.js +0 -4
  272. package/dist/test/suites/build/manager.test.js +0 -213
  273. package/dist/test/suites/main/client-bridge.test.js +0 -262
package/dist/main.js CHANGED
@@ -1,14 +1,17 @@
1
- // Main-process Manager singleton.
2
- // Consumer entry: `new (require('@omega.js/desktop/main'))().initialize()` — config auto-loads (config/omega.json5).
3
- // Boot sequence below — each step delegates to a `lib/*.js` module. Stubs today, real impls land in pass 2.
1
+ // The main-process runtime: ONE ready-made `omega` instance.
2
+ // Consumer entry: `const omega = require('@omega.js/desktop/main'); omega.initialize()`; config
3
+ // auto-loads (config/omega.json5). The boot sequence below delegates each step to a `lib/*.js` module.
4
4
 
5
5
  const LoggerLite = require('./lib/logger-lite.js');
6
6
  const { setEnvironment, ENVIRONMENT_VAR } = require('./vendor/config/environment.js');
7
+ const { createRequest } = require('@omega.js/client/modules/request.js');
8
+ const environment = require('./lib/_environment-mixin.js');
9
+ const lifecycle = require('./lib/_lifecycle-mixin.js');
7
10
 
8
11
  const storage = require('./lib/storage.js');
9
12
  const theme = require('./lib/theme.js');
10
13
  const fontawesome = require('./lib/fontawesome.js');
11
- // Error reporting is the shared @omega.js/monitoring contract (#380) — the
14
+ // Error reporting is the shared @omega.js/monitoring contract (#380): the
12
15
  // package's entry detects main vs renderer and forwards, exactly as the
13
16
  // lib/sentry/ split it replaced did.
14
17
  const sentry = require('./vendor/monitoring/index.js');
@@ -22,7 +25,7 @@ const tray = require('./lib/tray.js');
22
25
  const menu = require('./lib/menu.js');
23
26
  const ctxMenu = require('./lib/context-menu.js');
24
27
  const startup = require('./lib/startup.js');
25
- const wmBridge = require('./lib/client-bridge.js');
28
+ const auth = require('./lib/auth.js');
26
29
  const windows = require('./lib/window-manager.js');
27
30
  const context = require('./lib/context.js');
28
31
  const usage = require('./lib/usage.js');
@@ -31,499 +34,427 @@ const remoteScripts = require('./lib/remote-scripts.js');
31
34
  const analytics = require('./lib/analytics.js');
32
35
  const restartManager = require('./lib/restart-manager/index.js');
33
36
 
34
- function Manager() {
35
- const self = this;
36
-
37
- self.config = null;
38
- self.logger = new LoggerLite('main');
39
-
40
- // Quit-vs-hide gating. The window-manager `close` handler checks `_allowQuit` /
41
- // `_isQuitting` before deciding whether to actually close (=quit) or to swallow
42
- // the event and just hide the window. Set true via `manager.quit({ force: true })`,
43
- // by `app.on('before-quit')` (any user-initiated quit), and by the auto-updater
44
- // when it's about to call `quitAndInstall()`.
45
- self._allowQuit = false;
46
- self._isQuitting = false;
47
-
48
- // Public lib references (consumer code can call them by name)
49
- self.storage = storage;
50
- self.theme = theme;
51
- self.fontawesome = fontawesome;
52
- self.sentry = sentry;
53
- self.protocol = protocol;
54
- self.deepLink = deepLink;
55
- self.authFlow = authFlow;
56
- self.appState = appState;
57
- self.ipc = ipc;
58
- self.autoUpdater = autoUpdater;
59
- self.tray = tray;
60
- self.menu = menu;
61
- self.contextMenu = ctxMenu;
62
- self.startup = startup;
63
- self.omega = wmBridge;
64
- self.windows = windows;
65
- self.context = context;
66
- self.usage = usage;
67
- self.remoteConfig = remoteConfig;
68
- self.remoteScripts = remoteScripts;
69
- self.analytics = analytics;
70
- self.restartManager = restartManager;
71
-
72
- return self;
73
- }
74
-
75
- // Open the sign-in round trip in the user's default browser (lib/auth-flow.js).
76
- // Resolves once launched; completion arrives via the auth/token deep-link route →
77
- // omega.handleAuthToken → the desktop:auth:sign-in-with-token broadcast.
78
- Manager.prototype.openAuthFlow = function (options) {
79
- return authFlow.open(options);
80
- };
81
-
82
- // Force a real quit, bypassing per-window `hideOnClose`. Use this anywhere the
83
- // app legitimately wants to exit (tray Quit, Cmd+Q from menu role:'quit',
84
- // auto-updater install). Without `{ force: true }`, the close events still get
85
- // trapped by hide-on-close handlers and the app stays running.
86
- Manager.prototype.quit = function (options) {
87
- const self = this;
88
- options = options || {};
89
-
90
- if (options.force) {
91
- self._allowQuit = true;
92
- }
93
-
94
- try {
95
- require('electron').app.quit();
96
- } catch (e) { /* electron not available — no-op in test/headless */ }
97
- };
37
+ /**
38
+ * The main-process runtime. This module exports ONE instance of it; a consumer
39
+ * never writes `new`, and awaits `initialize()` or `ready`. Every library is a
40
+ * plain property (`omega.windows`, `omega.tray`, `omega.auth`, ...).
41
+ */
42
+ class Omega {
43
+ constructor() {
44
+ this.config = null;
45
+ this.logger = new LoggerLite('main');
46
+
47
+ // Quit-vs-hide gating. The window-manager `close` handler checks `_allowQuit` /
48
+ // `_isQuitting` before deciding whether to actually close (=quit) or to swallow
49
+ // the event and just hide the window. Set true via `omega.quit({ force: true })`,
50
+ // by `app.on('before-quit')` (any user-initiated quit), and by the auto-updater
51
+ // when it's about to call `quitAndInstall()`.
52
+ this._allowQuit = false;
53
+ this._isQuitting = false;
54
+
55
+ // Public lib references (consumer code can call them by name)
56
+ this.storage = storage;
57
+ this.theme = theme;
58
+ this.fontawesome = fontawesome;
59
+ this.sentry = sentry;
60
+ this.protocol = protocol;
61
+ this.deepLink = deepLink;
62
+ this.authFlow = authFlow;
63
+ this.appState = appState;
64
+ this.ipc = ipc;
65
+ this.autoUpdater = autoUpdater;
66
+ this.tray = tray;
67
+ this.menu = menu;
68
+ this.contextMenu = ctxMenu;
69
+ this.startup = startup;
70
+ this.auth = auth;
71
+ this.windows = windows;
72
+ this.context = context;
73
+ this.usage = usage;
74
+ this.remoteConfig = remoteConfig;
75
+ this.remoteScripts = remoteScripts;
76
+ this.analytics = analytics;
77
+ this.restartManager = restartManager;
78
+
79
+ // The harmonized API fetch (omega.request), the client base's shape: a
80
+ // fresh Bearer token from main's session when signed in
81
+ this._request = createRequest({
82
+ getApiUrl: () => this.getApiUrl(),
83
+ getIdToken: (force) => this.auth.getIdToken(force),
84
+ });
98
85
 
99
- // Force a relaunch — same gating as quit, but tells electron to start back up
100
- // after exit. If an update has been downloaded, prefers `quitAndInstall()` so
101
- // the user lands on the new version instead of the old one.
102
- Manager.prototype.relaunch = function (options) {
103
- const self = this;
104
- options = options || {};
86
+ // Settled by initialize(): resolves with the instance, rejects with the
87
+ // error initialize() rethrows. A module that did not call initialize() can
88
+ // still await it.
89
+ this._readyResolve = null;
90
+ this._readyReject = null;
91
+ this.ready = new Promise((resolve, reject) => {
92
+ this._readyResolve = resolve;
93
+ this._readyReject = reject;
94
+ });
105
95
 
106
- if (options.force) {
107
- self._allowQuit = true;
96
+ // A rejected boot nobody awaited must not surface as an unhandled
97
+ // rejection; a consumer awaiting `ready` still sees the error, because this
98
+ // catch hangs off a separate branch of the same promise.
99
+ this.ready.catch(() => {});
108
100
  }
109
101
 
110
- const electron = require('electron');
102
+ // Open the sign-in round trip in the user's default browser (lib/auth-flow.js).
103
+ // Resolves once launched; completion arrives via the auth/token deep-link route →
104
+ // auth.handleToken → the desktop:auth:sign-in-with-token broadcast.
105
+ openAuthFlow(options) {
106
+ return authFlow.open(options);
107
+ }
111
108
 
112
- // If updater downloaded a fresh build, install + relaunch via electron-updater
113
- // (which calls `app.quit()` internally with the right post-quit script). Falls
114
- // back to plain relaunch if updater hasn't downloaded anything.
115
- const updaterStatus = self.autoUpdater.getStatus();
116
- if (updaterStatus.code === 'downloaded') {
117
- return self.autoUpdater.installNow();
109
+ // Make an API request: `omega.request('/omega/user/token', { method: 'POST', body: {} })`,
110
+ // with @omega.js/client's options (`auth: false`, `output: 'complete'`, `wakeup: true`)
111
+ request(url, options) {
112
+ return this._request(url, options);
118
113
  }
119
114
 
120
- electron.app.relaunch();
121
- electron.app.quit();
122
- };
123
-
124
- Manager.prototype.initialize = async function (consumerConfig, options) {
125
- const self = this;
126
-
127
- // Accept either an already-RESOLVED config object, a string path to a consumer
128
- // project dir, or nothing. Default resolution order (when called with no arg):
129
- // 1. OMEGA_BUILD_JSON.config — injected at build time by the bundle task's esbuild `define`. This is
130
- // authoritative in packaged apps because config/omega.json5 is inside the asar —
131
- // not loadable from disk. It's the RESOLVED config (Manager.getConfig() output —
132
- // shared sections + targets.desktop overlaid) snapshotted at build time.
133
- // 2. <appRoot>/config/omega.json5 — resolved via @omega.js/config for dev mode where
134
- // @omega.js/desktop is loaded directly (unbundled). appRoot = the consumer project dir.
135
- if (typeof consumerConfig === 'string') {
136
- consumerConfig = loadResolvedConfig(consumerConfig);
137
- } else if (!consumerConfig) {
138
- // Try OMEGA_BUILD_JSON (set by DefinePlugin in packaged builds) first.
139
- if (typeof OMEGA_BUILD_JSON !== 'undefined' && OMEGA_BUILD_JSON?.config) {
140
- consumerConfig = OMEGA_BUILD_JSON.config;
141
- } else {
142
- const appRoot = require('./utils/app-root.js')();
143
- consumerConfig = loadResolvedConfig(appRoot);
115
+ /**
116
+ * Boot the main process in the fixed order (docs/boot-sequence.md), and
117
+ * settle `ready`.
118
+ * @param {object|string} [consumerConfig] - a RESOLVED config, a project dir to resolve one from, or nothing.
119
+ * @param {object} [options] - boot options (the test harness passes `skipWindowCreation`).
120
+ * @returns {Promise<Omega>} the instance.
121
+ */
122
+ async initialize(consumerConfig, options) {
123
+ try {
124
+ await this._boot(consumerConfig, options);
125
+ } catch (error) {
126
+ this._readyReject(error);
127
+ throw error;
144
128
  }
145
- }
146
129
 
147
- self.config = consumerConfig || {};
148
- self._options = options || {};
149
-
150
- // The environment's ONE input is the `OMEGA_ENVIRONMENT` variable
151
- // ([#817](https://github.com/Omega-JS-Stack/omega/issues/817)), and a lane
152
- // that named one is NEVER overridden here
153
- // ([#925](https://github.com/Omega-JS-Stack/omega/issues/925)). The baked word
154
- // is the FALLBACK, for the one case with no parent lane to inherit from: a
155
- // packaged app, where this is the input every later read answers (the
156
- // context-free ones, like test-stealth's, included). A dev boot inherits the
157
- // word from the gulp lane that spawned this electron, and the test lanes spawn
158
- // their child with `testing` while booting a PRODUCTION artifact, so writing
159
- // the baked word over theirs turned every isTesting() gate in a booted app
160
- // silently off.
161
- if (!process.env[ENVIRONMENT_VAR] && self.config.environment) {
162
- setEnvironment(self.config.environment);
163
- }
130
+ this._readyResolve(this);
164
131
 
165
- {
166
- const fs = require('fs');
167
- const _path = require('path');
168
- const _app = require('electron').app;
132
+ return this;
133
+ }
169
134
 
170
- const _productName = self.config.app?.productName || self.config.brand?.name;
171
- if (_productName) {
172
- _app.setName(_productName);
135
+ async _boot(consumerConfig, options) {
136
+ // Accept either an already-RESOLVED config object, a string path to a consumer
137
+ // project dir, or nothing. Default resolution order (when called with no arg):
138
+ // 1. OMEGA_BUILD_JSON.config: injected at build time by the bundle task's esbuild `define`. This is
139
+ // authoritative in packaged apps because config/omega.json5 is inside the asar and
140
+ // not loadable from disk. It's the RESOLVED config (build.getConfig() output:
141
+ // shared sections + targets.desktop overlaid) snapshotted at build time.
142
+ // 2. <appRoot>/config/omega.json5: resolved via @omega.js/config for dev mode where
143
+ // @omega.js/desktop is loaded directly (unbundled). appRoot = the consumer project dir.
144
+ if (typeof consumerConfig === 'string') {
145
+ consumerConfig = loadResolvedConfig(consumerConfig);
146
+ } else if (!consumerConfig) {
147
+ // Try OMEGA_BUILD_JSON (set by DefinePlugin in packaged builds) first.
148
+ if (typeof OMEGA_BUILD_JSON !== 'undefined' && OMEGA_BUILD_JSON?.config) {
149
+ consumerConfig = OMEGA_BUILD_JSON.config;
150
+ } else {
151
+ const appRoot = require('./utils/app-root.js')();
152
+ consumerConfig = loadResolvedConfig(appRoot);
153
+ }
173
154
  }
174
- const _logPath = _app.isPackaged
175
- ? _path.join(_app.getPath('logs'), 'runtime.log')
176
- : _path.join(process.cwd(), 'logs', 'runtime.log');
177
- try { fs.writeFileSync(_logPath, ''); } catch (e) {}
178
- }
179
155
 
180
- self.logger.log(`Initializing @omega.js/desktop (main)... pid=${process.pid} platform=${process.platform} arch=${process.arch} packaged=${require('electron').app.isPackaged} argv=${JSON.stringify(process.argv.slice(1))}`);
181
-
182
- try { if (require('electron').app.isPackaged) {
183
- const { execFile } = require('child_process');
184
- if (process.platform === 'darwin') {
185
- const _appIdx = process.execPath.indexOf('.app');
186
- if (_appIdx === -1) throw new Error('no .app in execPath');
187
- const _appPath = process.execPath.substring(0, _appIdx + 4);
188
- execFile('codesign', ['--verify', '--deep', '--strict', _appPath], (err) => {
189
- if (err) {
190
- self.logger.warn(`signing: NOT signed (${err.message.split('\n')[0]})`);
191
- } else {
192
- execFile('codesign', ['-dv', _appPath], { encoding: 'utf8' }, (e2, stdout, stderr) => {
193
- const authority = (stderr || '').match(/Authority=(.+)/);
194
- const adhoc = (stderr || '').includes('Signature=adhoc');
195
- const label = adhoc ? 'ad-hoc' : (authority ? authority[1] : 'unknown');
196
- self.logger.log(`signing: signed (${label})`);
197
- });
198
- }
199
- });
200
- } else if (process.platform === 'win32') {
201
- execFile('powershell', ['-NoProfile', '-Command', `(Get-AuthenticodeSignature '${process.execPath}').Status`], { encoding: 'utf8' }, (err, stdout) => {
202
- const status = (stdout || '').trim();
203
- if (err || status === 'NotSigned') {
204
- self.logger.warn(`signing: NOT signed`);
205
- } else {
206
- self.logger.log(`signing: signed (${status})`);
207
- }
208
- });
209
- } else {
210
- self.logger.log('signing: n/a (Linux — no OS-level code signing)');
156
+ this.config = consumerConfig || {};
157
+ this._options = options || {};
158
+
159
+ // The environment's ONE input is the `OMEGA_ENVIRONMENT` variable
160
+ // ([#817](https://github.com/Omega-JS-Stack/omega/issues/817)), and a lane
161
+ // that named one is NEVER overridden here
162
+ // ([#925](https://github.com/Omega-JS-Stack/omega/issues/925)). The baked word
163
+ // is the FALLBACK, for the one case with no parent lane to inherit from: a
164
+ // packaged app, where this is the input every later read answers (the
165
+ // context-free ones, like test-stealth's, included). A dev boot inherits the
166
+ // word from the gulp lane that spawned this electron, and the test lanes spawn
167
+ // their child with `testing` while booting a PRODUCTION artifact, so writing
168
+ // the baked word over theirs turned every isTesting() gate in a booted app
169
+ // silently off.
170
+ if (!process.env[ENVIRONMENT_VAR] && this.config.environment) {
171
+ setEnvironment(this.config.environment);
211
172
  }
212
- } } catch (e) { self.logger.warn(`signing: check failed (${e.message})`); }
213
-
214
- // Schema validation. Hard-fail boot if required fields are missing — same rules as
215
- // gulp/audit (single source of truth in @omega.js/config: shared schema + the desktop
216
- // target refinements). We do this before any lib initializes so a misconfigured app
217
- // fails loud + early instead of partway through boot with a confusing stack trace.
218
- {
219
- const { validateConfig, formatErrors } = require('./vendor/config/index.js');
220
- const { errors } = validateConfig(self.config, { target: 'desktop' });
221
- if (errors.length > 0) {
222
- throw new Error(`@omega.js/desktop: config validation failed — fix the following in config/omega.json5:\n${formatErrors(errors)}`);
173
+
174
+ {
175
+ const fs = require('fs');
176
+ const _path = require('path');
177
+ const _app = require('electron').app;
178
+
179
+ const _productName = this.config.app?.productName || this.config.brand?.name;
180
+ if (_productName) {
181
+ _app.setName(_productName);
182
+ }
183
+ const _logPath = _app.isPackaged
184
+ ? _path.join(_app.getPath('logs'), 'runtime.log')
185
+ : _path.join(process.cwd(), 'logs', 'runtime.log');
186
+ try { fs.writeFileSync(_logPath, ''); } catch (e) {}
223
187
  }
224
- }
225
188
 
226
- // electron is a peer dep — main process only (we're in main.js, always defined).
227
- const electron = require('electron');
228
- const app = electron.app;
229
-
230
- // Lifecycle event logging. These are the high-signal app-level events worth tracing
231
- // when something goes wrong — quit reasons, window-all-closed, will-finish-launching,
232
- // ready, render-process-gone, child-process-gone. All cheap to log; one line each.
233
- app.on('before-quit', () => {
234
- self._isQuitting = true;
235
- self.logger.log('app event: before-quit (entering quit sequence — close events bypass hide-on-close)');
236
- });
237
- app.on('will-quit', () => self.logger.log('app event: will-quit'));
238
- app.on('quit', (_e, exitCode) => self.logger.log(`app event: quit code=${exitCode}`));
239
- app.on('window-all-closed', () => self.logger.log('app event: window-all-closed'));
240
- app.on('render-process-gone', (_e, webContents, details) => self.logger.warn(`app event: render-process-gone reason=${details.reason} exitCode=${details.exitCode}`));
241
- app.on('child-process-gone', (_e, details) => self.logger.warn(`app event: child-process-gone type=${details.type} reason=${details.reason} exitCode=${details.exitCode}`));
242
- app.on('activate', () => self.logger.log('app event: activate (macOS — dock click or app re-launch)'));
243
- app.on('open-url', (_e, url) => self.logger.log(`app event: open-url url=${url}`));
244
- app.on('open-file', (_e, p) => self.logger.log(`app event: open-file path=${p}`));
245
-
246
- // Process-level signals that bypass Electron's app events. Catches uncaught exceptions
247
- // so we never crash silently — combined with electron-log file transport, this means
248
- // ANY unhandled throw lands in runtime.log rather than disappearing into stderr.
249
- process.on('uncaughtException', (e) => {
250
- // EPIPE = stdout/stderr pipe closed (CLI consumer hung up). Logging it would
251
- // write to console, which triggers another EPIPE, cascading into thousands of
252
- // identical log entries. Exit cleanly — standard Unix behavior.
253
- if (e?.code === 'EPIPE') {
254
- process.exit(0);
255
- return;
189
+ this.logger.log(`Initializing @omega.js/desktop (main)... pid=${process.pid} platform=${process.platform} arch=${process.arch} packaged=${require('electron').app.isPackaged} argv=${JSON.stringify(process.argv.slice(1))}`);
190
+
191
+ require('./utils/signing-status.js')(this.logger);
192
+
193
+ // Schema validation. Hard-fail boot if required fields are missing: same rules as
194
+ // gulp/audit (single source of truth in @omega.js/config: shared schema + the desktop
195
+ // target refinements). We do this before any lib initializes so a misconfigured app
196
+ // fails loud + early instead of partway through boot with a confusing stack trace.
197
+ {
198
+ const { validateConfig, formatErrors } = require('./vendor/config/index.js');
199
+ const { errors } = validateConfig(this.config, { target: 'desktop' });
200
+ if (errors.length > 0) {
201
+ throw new Error(`@omega.js/desktop: config validation failed. Fix the following in config/omega.json5:\n${formatErrors(errors)}`);
202
+ }
256
203
  }
257
- self.logger.error(`uncaughtException: ${e?.stack || e?.message || String(e)}`);
258
- });
259
- process.on('unhandledRejection', (reason) => {
260
- self.logger.error(`unhandledRejection: ${reason?.stack || reason?.message || String(reason)}`);
261
- });
262
- process.on('exit', (code) => {
263
- // electron-log's file transport flushes synchronously, so this last line lands.
264
- self.logger.log(`process exit code=${code}`);
265
- });
266
-
267
- // 1. Apply early startup-mode hide. For `mode: 'hidden'` we call app.dock.hide() *before*
268
- // anything else so macOS spends as little time animating the dock entry as possible.
269
- // In packaged builds the Info.plist's LSUIElement (injected by gulp/build-config when
270
- // startup.mode === 'hidden') prevents the bounce entirely.
271
- self.startup._manager = self;
272
- self.startup._electron = electron || null;
273
- self.startup.applyEarly();
274
-
275
- // 1a. Test stealth: suppress app-level activation on macOS. Launching a regular-
276
- // policy app activates it — menu bar + keyboard focus switch away from whatever
277
- // the developer is typing in — even though stealth windows surface via
278
- // showInactive() (see lib/window-manager.js). The accessory policy (the same
279
- // switch app.dock.hide() flips, and what LSUIElement bakes for packaged
280
- // hidden-mode apps) keeps the test process from ever activating; windows still
281
- // render normally. Must run before app ready — activation happens when the app
282
- // finishes launching. OMEGA_TEST_SHOW=1 restores normal activation along with
283
- // visible windows.
284
- if (process.platform === 'darwin' && require('./utils/test-stealth.js')(self)) {
285
- app.dock.hide();
286
- self.logger.log('test stealth: app activation suppressed (dock hidden / accessory policy) — launch will not steal focus');
287
- }
288
204
 
289
- // 1a-ii. Test stealth for EVERY BrowserWindow — including RAW ones created with
290
- // `new BrowserWindow()` that never pass through lib/window-manager (e.g.
291
- // a consumer's automation popup). Window-manager stealths only its own
292
- // named windows via _surface(); this hook closes the gap so no window
293
- // can flash or steal focus during a test run. The predicate is evaluated
294
- // PER WINDOW so OMEGA_TEST_SHOW=1 keeps working even when flipped mid-run
295
- // (the window-manager suite does exactly that).
296
- if (self.isTesting()) {
297
- app.on('browser-window-created', (_event, win) => {
298
- if (!require('./utils/test-stealth.js')(self)) return;
299
- require('./utils/stealth-window.js').applyStealth(win);
205
+ // electron is a peer dep, main process only (we're in main.js, always defined).
206
+ const electron = require('electron');
207
+ const app = electron.app;
208
+
209
+ // Lifecycle event logging. These are the high-signal app-level events worth tracing
210
+ // when something goes wrong: quit reasons, window-all-closed, will-finish-launching,
211
+ // ready, render-process-gone, child-process-gone. All cheap to log; one line each.
212
+ app.on('before-quit', () => {
213
+ this._isQuitting = true;
214
+ this.logger.log('app event: before-quit (entering quit sequence: close events bypass hide-on-close)');
300
215
  });
301
- // webContents.focus() is NOT covered by the window stealth above — it's a
302
- // different object whose focus() reaches the native window directly,
303
- // making the invisible window KEY: the test app grabs the keyboard from
304
- // whatever the developer is typing in (the accessory policy from 1a does
305
- // not prevent key-window steals, only launch activation). Consumers call
306
- // it legitimately (address-bar focus on tab select, overlay dismiss
307
- // hand-back), so no-op it per-contents under the same predicate —
308
- // synthetic input (executeJavaScript, sendInputEvent, CDP) targets a
309
- // specific webContents and never depends on OS key status.
310
- app.on('web-contents-created', (_event, wc) => {
311
- if (!require('./utils/test-stealth.js')(self)) return;
312
- wc.focus = () => {};
216
+ app.on('will-quit', () => this.logger.log('app event: will-quit'));
217
+ app.on('quit', (_e, exitCode) => this.logger.log(`app event: quit code=${exitCode}`));
218
+ app.on('window-all-closed', () => this.logger.log('app event: window-all-closed'));
219
+ app.on('render-process-gone', (_e, webContents, details) => this.logger.warn(`app event: render-process-gone reason=${details.reason} exitCode=${details.exitCode}`));
220
+ app.on('child-process-gone', (_e, details) => this.logger.warn(`app event: child-process-gone type=${details.type} reason=${details.reason} exitCode=${details.exitCode}`));
221
+ app.on('activate', () => this.logger.log('app event: activate (macOS: dock click or app re-launch)'));
222
+ app.on('open-url', (_e, url) => this.logger.log(`app event: open-url url=${url}`));
223
+ app.on('open-file', (_e, p) => this.logger.log(`app event: open-file path=${p}`));
224
+
225
+ // Process-level signals that bypass Electron's app events. Catches uncaught exceptions
226
+ // so we never crash silently: combined with electron-log file transport, this means
227
+ // ANY unhandled throw lands in runtime.log rather than disappearing into stderr.
228
+ process.on('uncaughtException', (e) => {
229
+ // EPIPE = stdout/stderr pipe closed (CLI consumer hung up). Logging it would
230
+ // write to console, which triggers another EPIPE, cascading into thousands of
231
+ // identical log entries. Exit cleanly: standard Unix behavior.
232
+ if (e?.code === 'EPIPE') {
233
+ process.exit(0);
234
+ return;
235
+ }
236
+ this.logger.error(`uncaughtException: ${e?.stack || e?.message || String(e)}`);
237
+ });
238
+ process.on('unhandledRejection', (reason) => {
239
+ this.logger.error(`unhandledRejection: ${reason?.stack || reason?.message || String(reason)}`);
240
+ });
241
+ process.on('exit', (code) => {
242
+ // electron-log's file transport flushes synchronously, so this last line lands.
243
+ this.logger.log(`process exit code=${code}`);
313
244
  });
314
- self.logger.log('test stealth: every BrowserWindow (raw ones included) surfaces invisible + unfocusable, webContents.focus() suppressed (OMEGA_TEST_SHOW=1 to watch)');
315
- }
316
245
 
317
- // 1b. Isolate the userData path per environment. MUST run before
318
- // storage.initialize() because electron-store reads `app.getPath('userData')`
319
- // at construction time.
320
- // production → <name> (untouched)
321
- // development → <name> (Development) (dev runs never touch installed-app data)
322
- // testing → <name> (Testing) (wiped at boot — every test run starts
323
- // from a clean slate; post-run state stays
324
- // on disk for inspection until the next run.
325
- // Set OMEGA_TEST_KEEP_USERDATA=1 to skip the wipe.)
326
- if (self.isTesting()) {
327
- const before = app.getPath('userData');
328
- const after = `${before} (Testing)`;
329
- const keep = process.env.OMEGA_TEST_KEEP_USERDATA === '1';
330
- if (!keep) {
331
- require('fs').rmSync(after, { recursive: true, force: true });
246
+ // 1. Apply early startup-mode hide. For `mode: 'hidden'` we call app.dock.hide() *before*
247
+ // anything else so macOS spends as little time animating the dock entry as possible.
248
+ // In packaged builds the Info.plist's LSUIElement (injected by gulp/build-config when
249
+ // startup.mode === 'hidden') prevents the bounce entirely.
250
+ this.startup._omega = this;
251
+ this.startup._electron = electron || null;
252
+ this.startup.applyEarly();
253
+
254
+ // 1a. Test stealth: suppress app-level activation on macOS. Launching a regular-
255
+ // policy app activates it (menu bar + keyboard focus switch away from whatever
256
+ // the developer is typing in) even though stealth windows surface via
257
+ // showInactive() (see lib/window-manager.js). The accessory policy (the same
258
+ // switch app.dock.hide() flips, and what LSUIElement bakes for packaged
259
+ // hidden-mode apps) keeps the test process from ever activating; windows still
260
+ // render normally. Must run before app ready: activation happens when the app
261
+ // finishes launching. OMEGA_TEST_SHOW=1 restores normal activation along with
262
+ // visible windows.
263
+ if (process.platform === 'darwin' && require('./utils/test-stealth.js')(this)) {
264
+ app.dock.hide();
265
+ this.logger.log('test stealth: app activation suppressed (dock hidden / accessory policy), launch will not steal focus');
332
266
  }
333
- app.setPath('userData', after);
334
- self.logger.log(`userData path: ${before} -> ${after} (testing mode, ${keep ? 'kept' : 'wiped at boot'})`);
335
- } else if (!self.isProduction()) {
336
- const before = app.getPath('userData');
337
- const after = `${before} (Development)`;
338
- app.setPath('userData', after);
339
- self.logger.log(`userData path: ${before} -> ${after} (dev mode)`);
340
- } else {
341
- self.logger.log(`userData path: ${app.getPath('userData')} (production)`);
342
- }
343
267
 
344
- // 1c. Set the global user agent fallback. Default template applied to every @omega.js/desktop app so
345
- // web requests (BrowserWindow loads, fetch, electron-updater downloads) carry a
346
- // branded UA — Mozilla parsers see a normal Chrome UA + we tag with the app's
347
- // name/version for our own server-side telemetry. Legacy @omega.js/desktop did the
348
- // same; merge tags now use node-powertools.template (single-curly syntax).
349
- {
350
- const { template } = require('node-powertools');
351
- const ctx = {
352
- brand: {
353
- name: self.config.brand.name,
354
- id: self.config.brand.id,
355
- },
356
- app: {
357
- version: self.getVersion(),
358
- },
359
- chrome: process.versions.chrome,
360
- electron: process.versions.electron,
361
- node: process.versions.node,
362
- platform: process.platform,
363
- arch: process.arch,
364
- };
365
- const templates = {
366
- darwin: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) {brand.name}/{app.version} Chrome/{chrome} Safari/537.36',
367
- win32: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) {brand.name}/{app.version} Chrome/{chrome} Safari/537.36',
368
- linux: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) {brand.name}/{app.version} Chrome/{chrome} Safari/537.36',
369
- };
370
- const tmpl = templates[process.platform] || templates.linux;
371
- const ua = template(tmpl, ctx);
372
- app.userAgentFallback = ua;
373
- self.logger.log(`userAgent: ${ua}`);
374
- }
268
+ // 1a-ii. Test stealth for EVERY BrowserWindow, including RAW ones created with
269
+ // `new BrowserWindow()` that never pass through lib/window-manager (e.g.
270
+ // a consumer's automation popup). Window-manager stealths only its own
271
+ // named windows via _surface(); this hook closes the gap so no window
272
+ // can flash or steal focus during a test run. The predicate is evaluated
273
+ // PER WINDOW so OMEGA_TEST_SHOW=1 keeps working even when flipped mid-run
274
+ // (the window-manager suite does exactly that).
275
+ if (this.isTesting()) {
276
+ app.on('browser-window-created', (_event, win) => {
277
+ if (!require('./utils/test-stealth.js')(this)) return;
278
+ require('./utils/stealth-window.js').applyStealth(win);
279
+ });
280
+ // webContents.focus() is NOT covered by the window stealth above: it's a
281
+ // different object whose focus() reaches the native window directly,
282
+ // making the invisible window KEY, so the test app grabs the keyboard from
283
+ // whatever the developer is typing in (the accessory policy from 1a does
284
+ // not prevent key-window steals, only launch activation). Consumers call
285
+ // it legitimately (address-bar focus on tab select, overlay dismiss
286
+ // hand-back), so no-op it per-contents under the same predicate.
287
+ // Synthetic input (executeJavaScript, sendInputEvent, CDP) targets a
288
+ // specific webContents and never depends on OS key status.
289
+ app.on('web-contents-created', (_event, wc) => {
290
+ if (!require('./utils/test-stealth.js')(this)) return;
291
+ wc.focus = () => {};
292
+ });
293
+ this.logger.log('test stealth: every BrowserWindow (raw ones included) surfaces invisible + unfocusable, webContents.focus() suppressed (OMEGA_TEST_SHOW=1 to watch)');
294
+ }
375
295
 
376
- // 2. IPC bus online first — storage and other libs register handlers on it
377
- self.ipc.initialize(self);
296
+ // 1b. Isolate the userData path per environment. MUST run before
297
+ // storage.initialize() because electron-store reads `app.getPath('userData')`
298
+ // at construction time.
299
+ // production → <name> (untouched)
300
+ // development → <name> (Development) (dev runs never touch installed-app data)
301
+ // testing → <name> (Testing) (wiped at boot: every test run starts
302
+ // from a clean slate; post-run state stays
303
+ // on disk for inspection until the next run.
304
+ // Set OMEGA_TEST_KEEP_USERDATA=1 to skip the wipe.)
305
+ if (this.isTesting()) {
306
+ const before = app.getPath('userData');
307
+ const after = `${before} (Testing)`;
308
+ const keep = process.env.OMEGA_TEST_KEEP_USERDATA === '1';
309
+ if (!keep) {
310
+ require('fs').rmSync(after, { recursive: true, force: true });
311
+ }
312
+ app.setPath('userData', after);
313
+ this.logger.log(`userData path: ${before} -> ${after} (testing mode, ${keep ? 'kept' : 'wiped at boot'})`);
314
+ } else if (!this.isProduction()) {
315
+ const before = app.getPath('userData');
316
+ const after = `${before} (Development)`;
317
+ app.setPath('userData', after);
318
+ this.logger.log(`userData path: ${before} -> ${after} (dev mode)`);
319
+ } else {
320
+ this.logger.log(`userData path: ${app.getPath('userData')} (production)`);
321
+ }
378
322
 
379
- // 3. Storage (precedes sentry + auth so opt-out + persisted session are honored)
380
- await self.storage.initialize(self);
323
+ // 1c. Set the global user agent fallback (utils/user-agent.js): a branded UA on
324
+ // every BrowserWindow load, fetch and electron-updater download.
325
+ require('./utils/user-agent.js')(this);
381
326
 
382
- // 3b. Theme — sets nativeTheme.themeSource from the persisted override / config
383
- // default, so every renderer (and native UI) resolves the right appearance
384
- // from its very first paint. Needs storage (override) + ipc (handlers) only.
385
- self.theme.initialize(self);
327
+ // 2. IPC bus online first: storage and other libs register handlers on it
328
+ this.ipc.initialize(this);
386
329
 
387
- // 3c. FontAwesome — serves the bundled icon SVGs to renderers over IPC
388
- // (desktop:fontawesome:get). Needs ipc only.
389
- self.fontawesome.initialize(self);
330
+ // 3. Storage (precedes sentry + auth so opt-out + persisted session are honored)
331
+ await this.storage.initialize(this);
390
332
 
391
- // 4. Sentry (earliest catchable global handler)
392
- self.sentry.initialize(self);
333
+ // 3b. Theme: sets nativeTheme.themeSource from the persisted override / config
334
+ // default, so every renderer (and native UI) resolves the right appearance
335
+ // from its very first paint. Needs storage (override) + ipc (handlers) only.
336
+ this.theme.initialize(this);
393
337
 
394
- // 5. Protocol (single-instance lock + custom URL scheme)
395
- self.protocol.initialize(self);
338
+ // 3c. FontAwesome: serves the bundled icon SVGs to renderers over IPC
339
+ // (desktop:fontawesome:get). Needs ipc only.
340
+ this.fontawesome.initialize(this);
396
341
 
397
- if (!self.protocol.hasSingleInstanceLock()) {
398
- // Quit the duplicate AND halt the boot: returning normally would resolve
399
- // initialize(), letting the consumer's .then() run its whole main.js (servers,
400
- // shared files, IPC) against a dying Electron — the duplicate must go quietly.
401
- // The returned promise intentionally never settles; the process exits first.
402
- self.logger.warn('Single-instance lock lost. Quitting this duplicate instance.');
403
- app.quit();
404
- return new Promise(() => {});
405
- }
342
+ // 4. Sentry (earliest catchable global handler)
343
+ this.sentry.initialize(this);
406
344
 
407
- // 6. Deep links (parse cold-start argv, install second-instance handler)
408
- self.deepLink.initialize(self);
409
-
410
- // 6b. Auth flow (the getAuthUrl sign-in round trip — external browser always;
411
- // dev/test return via a loopback listener since the scheme isn't OS-registered)
412
- self.authFlow.initialize(self);
413
-
414
- // 7. App state (first-launch / crash / startup-context flags)
415
- await self.appState.initialize(self);
416
-
417
- // 7b. Runtime context — session id, deviceId, OS info, async geolocation fetch.
418
- // Must run AFTER storage (writes deviceId + cached geolocation to storage) and
419
- // BEFORE analytics (which reads context.session.deviceId). Async but the geolocation
420
- // fetch is fire-and-forget so this returns quickly.
421
- await self.context.initialize(self);
422
-
423
- // 7c. Usage tracking — opens / hours-total / hours-this-session. Reads/writes
424
- // storage.usage and registers a before-quit handler to record session duration.
425
- self.usage.initialize(self);
426
-
427
- // 8. Wait for app readiness before any UI
428
- await app.whenReady();
429
-
430
- // 9. Auto-updater (queues check, never blocks UI)
431
- self.autoUpdater.initialize(self);
432
-
433
- // 10. Tray + menu + context menu
434
- self.tray.initialize(self);
435
- self.menu.initialize(self);
436
- self.contextMenu.initialize(self);
437
-
438
- // 11. Open-at-login + hide-on-startup state sync
439
- self.startup.initialize(self);
440
-
441
- // 12. Web-manager bridge (main-side Firebase Auth source of truth, IPC handlers for renderers)
442
- await self.omega.initialize(self);
443
-
444
- // 12b. Remote config — fetches `<brand.url>/data/resources/main.json` for hot
445
- // config flips (force-update gate, default user agents, etc.). Polls hourly.
446
- // Wired AFTER auto-updater so it can inherit feedCheckIntervalMs from there.
447
- self.remoteConfig.initialize(self);
448
-
449
- // 12c. Remote scripts — fetches `<brand.url>/data/scripts/main.json` for
450
- // emergency hotfixes (force-update, storage patches, etc.) when the normal
451
- // update pipeline is broken. Same polling cadence as remote-config.
452
- self.remoteScripts.initialize(self);
453
-
454
- // 12d. Analytics — GA4 via Measurement Protocol. Wired AFTER client-bridge
455
- // so it can subscribe to onAuthChange and flip user_id automatically.
456
- self.analytics.initialize(self);
457
-
458
- // 12e. Restart Manager — external guardian app that relaunches us if we crash.
459
- // Registers over RM's loopback HTTP protocol (runtime.json advertises the port),
460
- // heartbeats every 60s, deregisters on graceful quit, and silently installs RM
461
- // when missing (mac zip / win silent NSIS / linux AppImage — RM then self-updates
462
- // via its own @omega.js/desktop autoUpdater). Skips itself when this app IS restart-manager, in
463
- // dev (unless OMEGA_RESTART_MANAGER_DEV=1), or when restartManager.enabled=false.
464
- // See docs/restart-manager.md.
465
- self.restartManager.initialize(self);
466
-
467
- // 13. Initialize the windows lib — registers app-level handlers (window-all-closed, etc.)
468
- // but does NOT create any windows. Consumers are responsible for calling
469
- // `manager.windows.create('main')` (or any other named window) from their main.js
470
- // when they want to surface UI. This makes hidden / agent-app patterns trivial:
471
- // just don't call create() until something (tray click, deep link, IPC) warrants it.
472
- self.windows.initialize(self);
473
-
474
- self._initialized = true;
475
-
476
- // 14. Release deep-link dispatch — cold-start URLs (and any early open-url)
477
- // were queued so handlers like auth/token never fire before client-bridge
478
- // has Firebase up. Everything they touch exists now.
479
- self.deepLink.markManagerReady();
480
-
481
- self.logger.log('@omega.js/desktop (main) initialized.');
482
-
483
- // Boot test harness — runs against the live manager AFTER all libs are up. Test runner
484
- // sets OMEGA_TEST_BOOT=1 + OMEGA_TEST_BOOT_HARNESS=<absolute path> + OMEGA_TEST_BOOT_SPEC=<path>
485
- // before spawning electron. The harness emits __EM_TEST__ JSON lines on stdout (parsed
486
- // by runners/boot.js) then app.exit()s.
487
- //
488
- // We use a runtime env-var path (not a static `require('./test/harness/...')`) because
489
- // @omega.js/desktop is bundled into the consumer's bundle, and a static require would either get
490
- // inlined (bundling test code into production) or dead-code-eliminated. An env-var
491
- // path stays external and can only resolve when the runner sets it.
492
- if (process.env.OMEGA_TEST_BOOT === '1') {
493
- global.__em_manager = self;
494
- const harnessPath = process.env.OMEGA_TEST_BOOT_HARNESS;
495
- if (harnessPath) {
496
- // Defer the harness so the consumer's `manager.initialize().then(() => { ... })`
497
- // callback gets a chance to run first. @omega.js/desktop doesn't auto-create any windows; the
498
- // consumer's main.js does it inside .then(). If we ran the harness synchronously
499
- // here, that callback wouldn't have fired yet and `manager.windows.get('main')`
500
- // would be null. setImmediate flushes the microtask queue (where promise callbacks
501
- // live) before our boot harness inspects state.
502
- setImmediate(() => {
503
- try {
504
- // A variable specifier, so the bundler leaves the call alone and it
505
- // resolves at RUNTIME against the path the runner set — which is the
506
- // whole point (see the env-var note above).
507
- const harness = require(harnessPath);
508
- harness.run(self);
509
- } catch (e) {
510
- const { app } = require('electron');
511
- process.stdout.write(`__EM_TEST__${JSON.stringify({ event: 'fatal', message: `boot harness failed to load: ${e.message}` })}\n`);
512
- app.exit(1);
513
- }
514
- });
515
- } else {
516
- const { app } = require('electron');
517
- process.stdout.write(`__EM_TEST__${JSON.stringify({ event: 'fatal', message: 'OMEGA_TEST_BOOT=1 but OMEGA_TEST_BOOT_HARNESS not set' })}\n`);
518
- app.exit(1);
345
+ // 5. Protocol (single-instance lock + custom URL scheme)
346
+ this.protocol.initialize(this);
347
+
348
+ if (!this.protocol.hasSingleInstanceLock()) {
349
+ // Quit the duplicate AND halt the boot: returning normally would resolve
350
+ // initialize(), letting the consumer's .then() run its whole main.js (servers,
351
+ // shared files, IPC) against a dying Electron. The duplicate must go quietly.
352
+ // The returned promise intentionally never settles; the process exits first.
353
+ this.logger.warn('Single-instance lock lost. Quitting this duplicate instance.');
354
+ app.quit();
355
+ return new Promise(() => {});
519
356
  }
357
+
358
+ // 6. Deep links (parse cold-start argv, install second-instance handler)
359
+ this.deepLink.initialize(this);
360
+
361
+ // 6b. Auth flow (the getAuthUrl sign-in round trip: external browser always;
362
+ // dev/test return via a loopback listener since the scheme isn't OS-registered)
363
+ this.authFlow.initialize(this);
364
+
365
+ // 7. App state (first-launch / crash / startup-context flags)
366
+ await this.appState.initialize(this);
367
+
368
+ // 7b. Runtime context: session id, deviceId, OS info, async geolocation fetch.
369
+ // Must run AFTER storage (writes deviceId + cached geolocation to storage) and
370
+ // BEFORE analytics (which reads context.session.deviceId). Async but the geolocation
371
+ // fetch is fire-and-forget so this returns quickly.
372
+ await this.context.initialize(this);
373
+
374
+ // 7c. Usage tracking: opens / hours-total / hours-this-session. Reads/writes
375
+ // storage.usage and registers a before-quit handler to record session duration.
376
+ this.usage.initialize(this);
377
+
378
+ // 8. Wait for app readiness before any UI
379
+ await app.whenReady();
380
+
381
+ // 9. Auto-updater (queues check, never blocks UI)
382
+ this.autoUpdater.initialize(this);
383
+
384
+ // 10. Tray + menu + context menu
385
+ this.tray.initialize(this);
386
+ this.menu.initialize(this);
387
+ this.contextMenu.initialize(this);
388
+
389
+ // 11. Open-at-login + hide-on-startup state sync
390
+ this.startup.initialize(this);
391
+
392
+ // 12. Auth (main-side Firebase Auth source of truth, IPC handlers for renderers)
393
+ await this.auth.initialize(this);
394
+
395
+ // 12b. Remote config: fetches `<brand.url>/data/resources/main.json` for hot
396
+ // config flips (force-update gate, default user agents, etc.). Polls hourly.
397
+ // Wired AFTER auto-updater so it can inherit feedCheckIntervalMs from there.
398
+ this.remoteConfig.initialize(this);
399
+
400
+ // 12c. Remote scripts: fetches `<brand.url>/data/scripts/main.json` for
401
+ // emergency hotfixes (force-update, storage patches, etc.) when the normal
402
+ // update pipeline is broken. Same polling cadence as remote-config.
403
+ this.remoteScripts.initialize(this);
404
+
405
+ // 12d. Analytics: GA4 via Measurement Protocol. Wired AFTER auth so it can
406
+ // listen to it and flip user_id automatically.
407
+ this.analytics.initialize(this);
408
+
409
+ // 12e. Restart Manager: external guardian app that relaunches us if we crash.
410
+ // Registers over RM's loopback HTTP protocol (runtime.json advertises the port),
411
+ // heartbeats every 60s, deregisters on graceful quit, and silently installs RM
412
+ // when missing (mac zip / win silent NSIS / linux AppImage; RM then self-updates
413
+ // via its own @omega.js/desktop autoUpdater). Skips itself when this app IS restart-manager, in
414
+ // dev (unless OMEGA_RESTART_MANAGER_DEV=1), or when restartManager.enabled=false.
415
+ // See docs/restart-manager.md.
416
+ this.restartManager.initialize(this);
417
+
418
+ // 13. Initialize the windows lib: registers app-level handlers (window-all-closed, etc.)
419
+ // but does NOT create any windows. Consumers are responsible for calling
420
+ // `omega.windows.create('main')` (or any other named window) from their main.js
421
+ // when they want to surface UI. This makes hidden / agent-app patterns trivial:
422
+ // just don't call create() until something (tray click, deep link, IPC) warrants it.
423
+ this.windows.initialize(this);
424
+
425
+ this._initialized = true;
426
+
427
+ // 14. Release deep-link dispatch: cold-start URLs (and any early open-url)
428
+ // were queued so handlers like auth/token never fire before auth
429
+ // has Firebase up. Everything they touch exists now.
430
+ this.deepLink.markOmegaReady();
431
+
432
+ this.logger.log('@omega.js/desktop (main) initialized.');
433
+
434
+ // Boot test harness (utils/boot-harness.js): runs against the live instance AFTER
435
+ // all libs are up, only when the boot test lane spawned this process.
436
+ require('./utils/boot-harness.js')(this);
437
+ }
438
+
439
+ // Require: lets consumer main-process code load @omega.js/desktop's bundled dependencies at runtime
440
+ // (e.g. `omega.require('fs-jetpack')`). Resolves from @omega.js/desktop's module context, not the
441
+ // consumer's. Mirrors @omega.js/backend's omega.require(). For build-time resolution, the
442
+ // bundle task's framework-deps resolve hook handles this automatically.
443
+ require(name) {
444
+ return require(name);
520
445
  }
521
446
 
522
- return self;
523
- };
447
+ static require(name) {
448
+ return require(name);
449
+ }
450
+ }
451
+
452
+ // The environment and URL helpers, and quit/relaunch: methods on the instance,
453
+ // each concern in its own lib file
454
+ Object.assign(Omega.prototype, environment, lifecycle);
524
455
 
525
456
  function loadResolvedConfig(projectDir) {
526
- // @omega.js/config is vendored into dist (and bundled from there) — it
457
+ // @omega.js/config is vendored into dist (and bundled from there): it
527
458
  // finds config/omega.json5 under the project dir and resolves the desktop target
528
459
  // (shared sections + targets.desktop overlaid, brand-monorepo walk-up included).
529
460
  const { hasOmegaConfig, loadConfig } = require('./vendor/config/index.js');
@@ -535,24 +466,8 @@ function loadResolvedConfig(projectDir) {
535
466
  return loadConfig(projectDir, 'desktop').config;
536
467
  }
537
468
 
538
- // Cross-context helpers (isDevelopment/isProduction/isTesting + getFunctionsUrl/getApiUrl
539
- // + getEnvironment) live in src/utils/mode-helpers.js + src/utils/url-helpers.js +
540
- // src/build.js. All four Manager constructors mix them in via their respective
541
- // `attachTo(Manager)` calls — see the bottom of this file.
542
-
543
- // Require — lets consumer main-process code load @omega.js/desktop's bundled dependencies at runtime
544
- // (e.g. `manager.require('fs-jetpack')`). Resolves from @omega.js/desktop's module context, not the
545
- // consumer's. Mirrors @omega.js/backend's Manager.require(). For build-time resolution, the
546
- // bundle task's framework-deps resolve hook handles this automatically.
547
- Manager.prototype.require = function (name) {
548
- return require(name);
549
- };
550
- Manager.require = function (name) {
551
- return require(name);
552
- };
553
-
554
- // Mix in shared cross-context helpers — same code path used in renderer, preload, build.
555
- require('./utils/mode-helpers.js').attachTo(Manager);
556
- require('./utils/url-helpers.js').attachTo(Manager);
557
-
558
- module.exports = Manager;
469
+ // The ONE instance, initialized by the consumer's src/main.js
470
+ const omega = new Omega();
471
+
472
+ module.exports = omega;
473
+ module.exports.Omega = Omega;