@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,243 +0,0 @@
1
- # Auto-updater
2
-
3
- Wraps `electron-updater` with three triggers: startup check, periodic check, and a 30-day max-age gate that force-installs pending updates that have been ignored too long.
4
-
5
- ## Triggers
6
-
7
- | Trigger | When | Behavior |
8
- |---|---|---|
9
- | **Startup check** | `startupDelayMs` after `app.whenReady()` (default 10s) | Non-blocking. Fires once. |
10
- | **Feed check** | Every `feedCheckIntervalMs` (default 1h) | HTTP poll of the release feed; also re-evaluates the 30-day gate each tick. |
11
- | **Idle evaluation** | Every `idleEvalIntervalMs` (default 60s) | Cheap in-process check: install a downloaded update once the user has been idle long enough. |
12
- | **30-day gate** | When a download lands + every feed tick | If a pending update was downloaded ≥ `maxAgeMs` ago (default 30 days), force `quitAndInstall()`. A pending update carried from a prior session keeps its original `downloadedAt`, so the gate trips as soon as the startup check re-downloads it. |
13
- | **Manual check** | `omega.autoUpdater.checkNow()` (main) or `window.desktop.autoUpdater.checkNow()` (renderer) | Same as a periodic check but `userInitiated: true`. |
14
-
15
- ## State machine
16
-
17
- `status.code` is one of:
18
-
19
- ```
20
- idle — nothing happening
21
- checking — checking the feed
22
- available — feed says an update exists
23
- downloading — download in progress (status.percent updated)
24
- downloaded — fully downloaded; ready to install. Also: pendingUpdate.downloadedAt is set in storage.
25
- not-available — feed says no update
26
- error — checkForUpdates() or download failed; status.error.message has details
27
- ```
28
-
29
- ## Config (`config/omega.json5`)
30
-
31
- ```jsonc
32
- autoUpdate: {
33
- enabled: true,
34
- channel: 'latest', // latest / beta / alpha
35
- startupDelayMs: 10000, // 10s after whenReady
36
- feedCheckIntervalMs: 3600000, // 1h — release-feed poll cadence (HTTP; also re-checks the 30-day gate)
37
- idleEvalIntervalMs: 60000, // 60s — idle-install evaluator cadence (in-process)
38
- maxAgeMs: 2592000000, // 30 days; if pendingUpdate is older, force install
39
- autoDownload: true, // electron-updater downloads automatically
40
- }
41
- ```
42
-
43
- ## 30-day max-age gate
44
-
45
- The problem: if a user keeps their app open for weeks, an update may download but never apply. Eventually their version is dangerously stale (e.g. an unpatched security issue).
46
-
47
- The gate:
48
-
49
- 1. **First download wins.** When `update-downloaded` fires, @omega.js/desktop stores `pendingUpdate = { version, downloadedAt: Date.now() }` to `storage.autoUpdater.pendingUpdate`.
50
- 2. **Subsequent downloads do NOT reset the timer.** If a newer update downloads later, `downloadedAt` stays at the original time. (Otherwise the user could keep dodging by triggering re-checks.)
51
- 3. **When a download lands + every feed tick**, @omega.js/desktop checks if `Date.now() - downloadedAt >= maxAgeMs`. If yes → `quitAndInstall()`. Force. (At init the artifact isn't re-downloaded yet — the restored `downloadedAt` makes the gate trip the moment the startup check's download completes.)
52
- 4. **Cleared on apply.** When the app next launches and `app.getVersion() === pendingUpdate.version`, the flag is cleared automatically (the user successfully restarted into the new version).
53
-
54
- This guarantees no app on @omega.js/desktop stays > maxAgeMs days behind a downloaded update — provided `autoDownload` stays on (default): the cross-session gate enforces when the startup check's re-download lands, so with `autoDownload: false` it waits for the next download, whenever the consumer triggers one.
55
-
56
- ## Idle-aware install (15-min default)
57
-
58
- When an update finishes downloading via a background poll (NOT a user-initiated check), @omega.js/desktop does NOT immediately quit-and-install. Two independent timers own the decision: the feed-check timer (`feedCheckIntervalMs`, HTTP, also enforces the 30-day gate) and the idle-eval timer (`idleEvalIntervalMs`, cheap in-process arithmetic) which installs the downloaded update once the user has been idle past the threshold. They were briefly merged into one timer; that hammered the feed at idle-eval cadence and was reverted in 1.3.1.
59
-
60
- ### Activity signals
61
-
62
- Any UI activity bumps `_lastActivityAt = Date.now()`. Built-in signals:
63
-
64
- - **Renderer-side** — `mousedown`, `keydown`, `wheel`, `touchstart`, `focus` on `window` (capture phase, debounced to once per 5s in preload; sent to main as IPC `desktop:auto-updater:activity` which routes through `_onActivityIpc → markActive`).
65
- - **Main-side** — `app.on('browser-window-focus')` (covers tray-click-to-show, dock click, alt-tab back, etc.) wired during `_wireActivityHooks()` (idempotent, one-shot per process).
66
-
67
- ### Decision flow (`_evaluateIdleInstall`)
68
-
69
- Runs every periodic tick. Conditions, in order:
70
-
71
- - If `state.code !== 'downloaded'` → no-op (nothing to install).
72
- - If `_userInitiated` → no-op (consumer UI owns the install affordance).
73
- - If `_isDevMode()` → no-op (dev simulator's `quitAndInstall` is a no-op anyway).
74
- - If `Date.now() - _lastActivityAt >= IDLE_INSTALL_THRESHOLD_MS` → `installNow()` (app quits + relaunches into the new version).
75
- - Else if `_promptedForVersion !== state.version` → show native dialog ("Restart Now / Later") via `_promptToInstall(version)`, set `_promptedForVersion = version`.
76
- - Else → no-op this tick. Try again next tick.
77
-
78
- Constants hardcoded at the top of `src/lib/auto-updater.js`:
79
- - `IDLE_INSTALL_THRESHOLD_MS = 15 * 60 * 1000` — how long the user must be idle.
80
-
81
- ### User-initiated checks bypass everything
82
-
83
- When someone clicks "Check for Updates" in the menu/tray, `checkNow({userInitiated: true})` fires. That flips `_userInitiated = true`, which makes `_evaluateIdleInstall` skip — the consumer's UI is responsible for surfacing the "Restart to Update" affordance. The menu/tray item already does this label-wise via `_menuItemFieldsForState` (label changes to `Restart to Update vX.Y.Z` + enabled).
84
-
85
- ### `checkNow()` dedup + `_userInitiated` leak fix
86
-
87
- `_readyToCheck()` returns true only when `state.code` is `idle | not-available | error`. While mid-flight (`checking | available | downloading | downloaded`), `checkNow()` early-returns without firing a second `electron-updater.checkForUpdates()`. Combined with Electron's `ipcMain.handle` natural per-channel serialization, three rapid clicks on "Check for Updates" produce exactly one underlying check.
88
-
89
- Subtle: `_userInitiated` is only flipped AFTER the `_readyToCheck` guard. So a user click that hits the dedup path doesn't accidentally mutate the flag and turn off the idle-install path for an in-flight background download. (Pre-1.2.39 had this bug.)
90
-
91
- ### Consumer hook: `markActive()`
92
-
93
- Consumers can force-bump the activity timestamp from anywhere:
94
-
95
- ```js
96
- omega.autoUpdater.markActive();
97
- ```
98
-
99
- Call this from app-specific signals the framework can't see — e.g. just received an auth event, finished a long renderer task, finished a backend sync. Use sparingly; the built-in renderer mouse/keyboard/focus signals cover almost everything.
100
-
101
- ### Why 15 minutes?
102
-
103
- Long enough that an actively-used app won't surprise-quit mid-task. Short enough that a user who minimizes the app and walks to lunch comes back to the new version. Tune `IDLE_INSTALL_THRESHOLD_MS` if your app's usage pattern is different.
104
-
105
- ### Implementation notes
106
-
107
- - `_promptedForVersion` tracks the version we've already shown the dialog for. Reset on `shutdown()`. If a *newer* update downloads later, the version flips and the prompt fires again (different version).
108
- - The first `_evaluateIdleInstall` after a download lands waits up to one tick (default 60s) before any prompt or install — gives an active user a small grace window to reach a natural pause before the dialog appears.
109
- - The 30-day gate firing short-circuits idle eval: `_feedCheckTick` returns after `_enforceMaxAgeGate()` returns true, since the install is already in flight.
110
-
111
- ### Test mode behavior
112
-
113
- When `omega.isTesting() === true` (the one input: `OMEGA_ENVIRONMENT=testing`), the auto-updater swaps in test-friendly defaults so a real download → idle wait → install can complete in seconds instead of minutes:
114
-
115
- - **Idle threshold**: `IDLE_INSTALL_THRESHOLD_MS_TESTING = 3000ms` (3 sec) instead of 15 min.
116
- - **Both timers**: `IDLE_TICK_MS_TESTING = 500ms` replaces `feedCheckIntervalMs` and `idleEvalIntervalMs`.
117
- - **`_promptToInstall` short-circuits** before invoking `dialog.showMessageBox`. The native dialog is modal + blocking + would pop a window the test process can't dismiss programmatically. In test mode the prompt logs `[testing] _promptToInstall(...) — skipped native dialog.` and returns. Tests that want to assert prompt behavior override `_promptToInstall` per-test (see `auto-updater.test.js`).
118
-
119
- This lets the framework's own integration tests drive the full sequence (`OMEGA_DEV_UPDATE=available` → state machine → 500ms tick → 3s idle threshold elapses → stubbed `installNow` fires) in ~5s. Consumers running their own tests should name `OMEGA_ENVIRONMENT=testing` to inherit the same defaults.
120
-
121
- ## Menu integration
122
-
123
- @omega.js/desktop's default menu template includes a "Check for Updates..." item with id `desktop:check-for-updates`. The auto-updater listens to its own status changes and updates the item's label + enabled state, VS Code-style:
124
-
125
- | State | Label | Enabled |
126
- |---|---|---|
127
- | `idle` / `error` | Check for Updates... | yes |
128
- | `checking` | Checking for Updates... | no |
129
- | `available` | Downloading Update v{version}... | no |
130
- | `downloading` | Downloading Update ({percent}%) | no |
131
- | `downloaded` | Restart to Update v{version} | yes (clicks `installNow()`) |
132
- | `not-available` | You're up to date | yes |
133
-
134
- Click handler defaults to `checkNow()` when not yet downloaded; `installNow()` when downloaded.
135
-
136
- Consumers can find / move / remove the item via `omega.menu.findItem('desktop:check-for-updates')` etc.: see [docs/menu.md](menu.md).
137
-
138
- ## Renderer surface
139
-
140
- Preload exposes `window.desktop.autoUpdater`:
141
-
142
- ```js
143
- // Get current state
144
- const status = await window.desktop.autoUpdater.getStatus();
145
- // → { code, version, percent, error, downloadedAt, lastCheckedAt }
146
-
147
- // Subscribe to updates
148
- const unsubscribe = window.desktop.autoUpdater.onStatus((status) => {
149
- console.log('update status →', status.code, status.version, status.percent);
150
- });
151
-
152
- // User-initiated check (e.g. "Check for updates" menu item)
153
- await window.desktop.autoUpdater.checkNow();
154
-
155
- // User-initiated install (after status === 'downloaded')
156
- await window.desktop.autoUpdater.installNow();
157
- ```
158
-
159
- Status is also broadcast on the IPC channel `desktop:auto-updater:status` after every state transition.
160
-
161
- ## Dev simulation
162
-
163
- Without a real update server you can validate the entire flow via env vars:
164
-
165
- ```bash
166
- # Simulate "update available" — full cascade through downloading → downloaded
167
- OMEGA_DEV_UPDATE=available npm start
168
-
169
- # Simulate "no update available" — lands in not-available
170
- OMEGA_DEV_UPDATE=unavailable npm start
171
-
172
- # Simulate a feed failure — lands in error
173
- OMEGA_DEV_UPDATE=error npm start
174
- ```
175
-
176
- In dev simulation mode, `quitAndInstall()` is a no-op (no actual restart) so you can step through the dialog flow without the app exiting.
177
-
178
- The simulated download is memory-only: it never writes the `pendingUpdate` storage record, and the reconciler discards a stored one carrying the simulator's reserved `999.0.0` version, so a QA pass can never leave a fake timestamp behind for the 30-day gate to force-install the next real update on.
179
-
180
- ### The menu trigger
181
-
182
- Relaunching once per scenario is a slow way to walk three outcomes, so the default menu carries the same cascade on demand. In development, **View → Developer → Simulate update** lists one item per scenario:
183
-
184
- | Item | ID | Outcome |
185
- |---|---|---|
186
- | Update available | `view/developer/simulate-update/available` | full cascade through downloading → downloaded |
187
- | No update available | `view/developer/simulate-update/unavailable` | lands in `not-available` |
188
- | Update error | `view/developer/simulate-update/error` | lands in `error` |
189
-
190
- Each item calls `omega.autoUpdater.simulate(scenario)`, which is callable from anywhere in main:
191
-
192
- ```js
193
- await omega.autoUpdater.simulate('available');
194
- ```
195
-
196
- Rules of the road:
197
-
198
- - It throws with `scenario required` when called with no argument, and on an unknown scenario (`available`, `unavailable`, `error` are the whole set, shared with the env var).
199
- - It **refuses in a packaged production build** unless that build was launched with `OMEGA_DEV_UPDATE` set. Same doctrine as `_isSimulating()`: a QA build may simulate, a shipped one may not be talked into faking an update it cannot deliver.
200
- - It **refuses while a cascade is still running** (one simulation at a time). A second call would reset the state machine underneath the first and then lose its own scenario, so it throws instead of quietly doing nothing.
201
- - It resolves with the status once the cascade is running (like `checkNow()`); the terminal state arrives over the usual `desktop:auto-updater:status` broadcast a few hundred ms later.
202
- - The first call swaps the real `electron-updater` instance out for the synthetic one and **leaves it swapped for the rest of the session**. Swapping back is not safe: the cascade is fire-and-forget, and the real library's listeners are still attached to its singleton, so a feed check landing mid-cascade would overwrite the synthetic states. Relaunch to get the real updater back.
203
-
204
- ### The session latch
205
-
206
- Because the synthetic library stays wired, every LATER trigger drives it too: the hourly feed-check tick calls `checkForUpdates()` with no scenario, the simulator falls back to `available`, and the state machine lands on a fake `downloaded v999.0.0`. So the first `simulate()` also latches the process into simulation mode, and `_isSimulating()` reads:
207
-
208
- ```js
209
- !!process.env.OMEGA_DEV_UPDATE || _devSimulationSession
210
- ```
211
-
212
- That latch is what keeps a synthetic update out of the real install path. Every existing guard is written against `_isSimulating()`, so with it set:
213
-
214
- - `_evaluateIdleInstall()` bails, so no native "restart to update" prompt fires for an update that does not exist.
215
- - `installNow()` bails before `omega._allowQuit = true` and `quitAndInstall()`.
216
-
217
- Without the latch, a plain dev session (env var unset) that clicked the menu once would hit all of the above on the next tick. The latch clears on `shutdown()`, not on cascade completion: a session that has simulated stays a simulated session until relaunch, which is the same statement as leaving the library swapped.
218
-
219
- The submenu is dev-only (same gate as `view/developer/toggle-devtools`) and is an ordinary menu item, so `omega.menu.remove('view/developer/simulate-update')` drops it like any other.
220
-
221
- ## Production: how electron-updater finds the feed
222
-
223
- `electron-updater` reads the `publish` block from the embedded `app-update.yml` (baked into the `.app` / `.exe` at build time by electron-builder). @omega.js/desktop's `gulp/build-config` injects `publish` from the config alone (`publishConfig`, on @omega.js/config's `releasesRepo`) into `dist/electron-builder.yml` before packaging, so the published `app-update.yml` points at:
224
-
225
- ```
226
- provider: github
227
- owner: <repo.org>
228
- repo: `${brand.id}-releases`
229
- releaseType: release
230
- ```
231
-
232
- So your private app repo and your public release repo are completely decoupled — the bundled binary knows where to look for updates. The address is never guessed from a git remote ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)): this feed URL is polled by every installed copy forever, and inside a brand monorepo the remote is the repo the brand is NESTED in.
233
-
234
- ## Failure modes
235
-
236
- - **Update repo isn't public** → `electron-updater` gets 404 or 401 against a private repo. Fix: ensure the releases repo (`<repo.org>/<brand.id>-releases`) is public; `omega deploy`'s precheck creates it that way.
237
- - **Token rotates / expires for `releases` repo** — `electron-updater` doesn't authenticate downloads (anonymous public reads). So tokens don't apply on the consumer side.
238
- - **`app-update.yml` missing in the packaged app** → look at the build log for `electron-builder`'s "creating updates yml" line. If it's skipped, your `publish` block didn't materialize correctly into `dist/electron-builder.yml`.
239
- - **`error` status with code `ERR_UPDATER_CHANNEL_FILE_NOT_FOUND`** → there's no `latest-mac.yml` (or `latest.yml` / `latest-linux.yml`) at the configured channel. Run a release first.
240
-
241
- ## Tests
242
-
243
- - `src/test/suites/main/auto-updater.test.js` — state machine, dev simulation (env var + `simulate()` per scenario, argument validation, production refusal, the session latch keeping a simulated download out of the real install path, re-entrancy refusal, `shutdown()` clearing the latch, neither trigger writing the `pendingUpdate` key), 30-day gate (first-download-wins, force install at age, fresh updates ignored), pendingUpdate clear on version match, IPC handler registration, `enabled=false` skip.
@@ -1,44 +0,0 @@
1
- # Boot Sequence
2
-
3
- `omega.initialize()` runs in the main process in a fixed order. Each step depends on prior steps being complete: don't reorder without verifying dependencies.
4
-
5
- ## Order
6
-
7
- 1. **`startup.applyEarly()`** — first thing, before `whenReady`. Calls `app.dock.hide()` for `mode: 'hidden'` (zero-bounce production via `LSUIElement` baked at build time).
8
- 1b. **userData path isolation** — appends an environment suffix to `app.getPath('userData')` so each environment's session data, logs, and `electron-store` files stay separate on the same machine: production untouched, development gets ` (Development)`, testing (`OMEGA_ENVIRONMENT=testing`) gets ` (Testing)`. The testing dir is **wiped at boot** so every test run starts from a clean slate (post-run state stays on disk for inspection until the next run; set `OMEGA_TEST_KEEP_USERDATA=1` to skip the wipe). **Must run before `storage.initialize()`** (which constructs `electron-store` against the path).
9
- 1c. **Global user-agent fallback** — sets `app.userAgentFallback` to a branded template via `node-powertools.template`. Default per-platform templates: `Mozilla/5.0 (... <platform-specific> ...) AppleWebKit/537.36 (KHTML, like Gecko) {brand.name}/{app.version} Chrome/{chrome} Safari/537.36`. Merge tags resolve from `{ brand: { name, id }, app: { version }, chrome, electron, node, platform, arch }`. Every BrowserWindow load + electron-updater fetch + node-fetch via the renderer carries the branded UA. Consumers can override post-init by re-setting `app.userAgentFallback` from their main.js.
10
- 2. **`app.on('before-quit')`** wired: sets `omega._isQuitting = true` so any quit path (Cmd+Q, role:'quit' menu, programmatic `app.quit()`, OS shutdown) bypasses the window-manager's hide-on-close trap.
11
- 3. **`ipc`** — typed channel bus online before any feature can register handlers.
12
- 4. **`storage`** — async (electron-store v11 ESM, bundled eagerly into `main.bundle.js` — see [storage.md](storage.md)). Other libs depend on this.
13
- 4b. **`theme`** — sets `nativeTheme.themeSource` from the persisted override (storage `theme.appearance`) → config `theme.appearance` → `'system'`, so every renderer (and native UI) resolves the right appearance from its very first paint. Needs storage + ipc only; must run before any window exists. See [themes.md](themes.md).
14
- 4c. **`fontawesome`**: serves the bundled icon SVGs to renderers over IPC (`desktop:fontawesome:get`). Needs ipc only.
15
- 5. **`sentry`** — earliest catchable global handler.
16
- 6. **`protocol`** — single-instance lock + custom scheme register.
17
- 7. **`deepLink`** — argv parse for cold-start, second-instance handler.
18
- 7b. **`authFlow`**: the sign-in round trip in the user's default browser (`omega.openAuthFlow()`); dev/test return through a loopback listener, since the scheme isn't OS-registered there.
19
- 8. **`appState`** — first-launch / launch-count / crash-sentinel / version-change.
20
- 8b. **`context`**: session id, deviceId, OS info, the async geolocation fetch. After storage (it writes deviceId), before analytics (which reads it).
21
- 8c. **`usage`**: opens / hours-total / hours-this-session, recorded on quit.
22
- 9. `await app.whenReady()`.
23
- 10. **`autoUpdater`** — electron-updater, never blocks.
24
- 11. **`tray`**, **`menu`**, **`contextMenu`**: file-based definitions from `src/integrations/{tray,menu,context-menu}/index.js`. Disable any of them at runtime via `omega.<name>.disable()` (no config flag).
25
- 12. **`startup.initialize`** — applies `setLoginItemSettings`.
26
- 13. **`auth`**: `omega.auth`, the main-side Firebase Auth source of truth, and the IPC handlers every renderer syncs through ([auth.md](auth.md)).
27
- 13b. **`remoteConfig`** — hot config from `<brand.url>/data/resources/main.json`. Non-blocking fire-and-forget fetch.
28
- 13c. **`remoteScripts`** — emergency remote code execution from `<brand.url>/data/scripts/main.js`. Non-blocking. Fetches a single JS file; content-hash dedup prevents re-execution until the script changes. Full main-process access.
29
- 13d. **`analytics`**: GA4 Measurement Protocol. Wired AFTER `auth` so it can subscribe with `omega.auth.listen()`.
30
- 13e. **`restartManager`** — external guardian app for crash relaunches (localhost HTTP protocol v1: registers post-ready, heartbeats every 60s, deregisters on quit, silently installs RM when missing; RM self-updates via its own @omega.js/desktop autoUpdater). See [restart-manager.md](restart-manager.md).
31
- 14. **`windows.initialize`**: registers app-level handlers: `window-all-closed` → quit on win/linux; `app.on('activate')` on macOS to surface `main` when the user double-clicks the dock icon (CleanMyMac-style). **Does NOT auto-create any window.** The consumer's main.js calls `omega.windows.create('main', { show: !startup.isLaunchHidden() })` from inside `omega.initialize().then(() => { ... })`. The `main` window is *always* created (so it's in the registry for the activate/second-instance handlers to find), but `show: false` keeps it invisible in hidden launches: tray icon shows immediately, dock icon + window appear only when something explicitly calls `windows.show('main')` (or the user double-clicks the running app).
32
- 15. **`deepLink.markOmegaReady()`**: releases the deep-link dispatch queue. Cold-start URLs (and any early `open-url`) wait here, so a route like `auth/token` never fires before `omega.auth` has Firebase up.
33
-
34
- ## Why this order
35
-
36
- - userData path append before storage init: otherwise dev and prod stores share the same path.
37
- - before-quit before window-manager: so window-manager's close-trap can read `_isQuitting`.
38
- - IPC before any other lib: every lib registers its own IPC handlers.
39
- - Storage before sentry: sentry persists scope data.
40
- - Theme right after storage: the persisted appearance override must hit `nativeTheme.themeSource` before any renderer paints, or the first frame flashes the wrong appearance.
41
- - protocol/deepLink before whenReady: argv parsing for cold-start deep links must beat first window creation.
42
- - whenReady gate is where Electron's app APIs become safe.
43
- - autoUpdater after whenReady but never blocks boot.
44
- - File-based integrations (tray/menu/context-menu) after everything that wires their click handlers (e.g. autoUpdater patches the check-for-updates menu item).
@@ -1,169 +0,0 @@
1
- # Build System
2
-
3
- @omega.js/desktop's pipeline: **prepare-package** (framework only) → **gulp** (consumer) → **esbuild** (3 bundles) → **electron-builder** (packaging) → **strategy-pluggable signing**.
4
-
5
- ## prepare-package (framework-side)
6
-
7
- Copies @omega.js/desktop's `src/` → `dist/` so consumers `require('@omega.js/desktop/main')` from the built output. Configured in @omega.js/desktop's `package.json`:
8
-
9
- ```jsonc
10
- "preparePackage": {
11
- "input": "./src",
12
- "output": "./dist",
13
- "type": "copy",
14
- "replace": {},
15
- "hooks": {}
16
- }
17
- ```
18
-
19
- Run with `npm start` (watch) or `npm run prepare` (one-shot).
20
-
21
- ## Gulp (consumer-side)
22
-
23
- Auto-loads tasks from `<@omega.js/desktop>/dist/gulp/tasks/*.js` via `<@omega.js/desktop>/dist/gulp/main.js`. Consumer's `package.json` points there:
24
-
25
- ```jsonc
26
- "scripts": {
27
- "gulp": "gulp --cwd ./ --gulpfile ./node_modules/@omega.js/desktop/dist/gulp/main.js"
28
- }
29
- ```
30
-
31
- ### Tasks
32
-
33
- | Task | Status | Description |
34
- |---|---|---|
35
- | `defaults` | real | Copy `<@omega.js/desktop>/dist/defaults/*` into the consumer (skips existing files) |
36
- | `distribute` | real | Stage consumer `src/` + @omega.js/desktop `dist/` into `.desktop-build/` |
37
- | `bundle` | real | Three parallel bundles — main / preload / renderer, through @omega.js/devkit's `bundle()` wrapper. Named `webpack` until [#737](https://github.com/Omega-JS-Stack/omega/issues/737) |
38
- | `sass` | real | SCSS → `dist/assets/css/*` |
39
- | `html` | real | `src/views/**/index.html` → `dist/views/*` |
40
- | `build-config` | real | Materialize `dist/electron-builder.yml` from source + mode-dependent injections (`LSUIElement` for hidden mode) |
41
- | `package` | real | Run `electron-builder build --config dist/electron-builder.yml` (full DMG/zip/universal-mac, NSIS-win, deb+AppImage-linux) |
42
- | `package-quick` | real | Quick-package for host platform/arch only — `--dir` mode, no DMG/zip/universal/notarize. ~30s vs ~3min for full `package`. Output: `release/<platform>-<arch>/<ProductName>.app` (or `.exe`-folder/linux-unpacked) — directly launchable. Used for smoke-testing packaged-mode behavior locally. `--quick` trims the electron-builder phase and NOTHING else: the build ahead of it is full and cold (#737). |
43
- | `release` | real | `electron-builder build --publish always` |
44
- | `audit` | real | Validate consumer config (required keys, valid enums, deep-link scheme format), ensure icon + entrypoints exist; in publish mode also requires an ADDRESSABLE releases repo (`repo.org` + `brand.id`) + `electron-builder.yml`. Throws with a numbered list of every problem found |
45
- | `serve` | real | Spawns `electron .` against the build output, websocket on `OMEGA_LIVERELOAD_PORT` |
46
-
47
- ### Composition
48
-
49
- ```js
50
- // `build` produces bundles only (dist/main.bundle.js, etc.) — no installer.
51
- exports.build = series(
52
- exports['hook:build:pre'],
53
- exports.defaults,
54
- exports.distribute,
55
- parallel(exports.sass, exports.bundle, exports.html),
56
- exports.audit,
57
- exports['build-config'],
58
- exports['hook:build:post'],
59
- );
60
-
61
- // `packageBuild` = build + electron-builder (full DMG/zip/universal). Slow (~3min on mac).
62
- exports.packageBuild = series(exports.build, exports.package);
63
-
64
- // `packageQuick` = build + electron-builder --dir for host platform/arch only.
65
- // Fast (~20-30s) — smoke-testing only.
66
- exports.packageQuick = series(exports.build, exports['package-quick']);
67
-
68
- // `publish` = build + sign + notarize + GH Release upload (to the brand's ONE
69
- // public releases repo, under versionless names).
70
- exports.publish = series(
71
- exports.build,
72
- exports['hook:release:pre'],
73
- exports.release,
74
- exports['hook:release:post'],
75
- );
76
-
77
- exports.default = series(exports.build, exports.serve);
78
- ```
79
-
80
- ## esbuild — three bundles
81
-
82
- All bundled in production for source protection. `app.asar` alone is not obfuscation (anyone can `npx asar extract` it) — minification and name mangling are what protect framework + app source.
83
-
84
- Every bundle goes through @omega.js/devkit's ONE `bundle()` wrapper ([docs/devkit/index.md](../../../docs/devkit/index.md)), which composes the shared parts: the framework-deps resolve hook (#87), the production `@dev-only` strip (#18), the minify/sourcemap rules by mode, and one timing line per build. `src/gulp/tasks/bundle.js` holds only what is desktop's own.
85
-
86
- | Bundle | Entry | Output | Platform / format | Externals |
87
- |---|---|---|---|---|
88
- | `main` | `src/main.js` | `dist/main.bundle.js` | `node` / `cjs` | electron + node builtins + native modules from consumer's `package.json` |
89
- | `preload` | `src/preload.js` | `dist/preload.bundle.js` | `node` / `cjs` | electron — `platform: 'node'` already leaves every built-in external, same as main, so electron is the only name worth stating |
90
- | `renderer` | `src/assets/js/components/<view>/index.js` | `dist/assets/js/components/<view>.bundle.js` | `browser` / `iife` | none — Node built-ins resolve to an empty module (see below) |
91
-
92
- ### Syntax floor — the pinned Electron answers it
93
-
94
- webpack encoded the runtime as `target: 'electron-main' | 'electron-preload' | 'web'`. esbuild splits it into `platform` (above) and `target` (the syntax floor), and the floor is READ from the Electron binary the consumer pinned: [src/utils/electron-targets.js](../src/utils/electron-targets.js) runs it once per build with `ELECTRON_RUN_AS_NODE` (no window, no focus) and takes `process.versions.node` → `node<version>` for main/preload and `process.versions.chrome` → `chrome<major>` for the renderer. The binary is resolved from the FRAMEWORK's module context — the same lookup `build-config` pins `electronVersion` with, so the bundles compile for the Electron that will actually run them. A binary that can't be run (a CI job with `ELECTRON_SKIP_BINARY_DOWNLOAD`) warns and drops the floor; it never invents a version.
95
-
96
- ### Node built-ins in the renderer
97
-
98
- The renderer runs with `contextIsolation: true` — a browser-like environment with no Node globals — but libraries bundled through @omega.js/client still IMPORT `fs`, `path`, `crypto` and friends on code paths their browser builds never take. webpack answered with `resolve.fallback: { fs: false, … }`; esbuild has no such option, so the same list is a resolve hook onto one empty CommonJS module (`RENDERER_EMPTY_MODULES` in the task). `electron` is on the list too: a renderer that reached the real module would be a security hole, not a missing polyfill.
99
-
100
- ### OMEGA_BUILD_JSON: a define for Node, one file for the browser
101
-
102
- The wrapper is the ONE shape every OMEGA browser surface carries, `{ config, package, mode, license, builtAt }` ([#894](https://github.com/Omega-JS-Stack/omega/issues/894)), with `mode` the same three keys everywhere (`{ environment, build, publish }`; desktop's own `server` verdict stays inside `build.getMode()`). Two blobs come out of one composition, off one set of build facts:
103
-
104
- - `composeBuildJson()` → main and preload, as an esbuild `define` (the bare identifier becomes the literal at compile time) plus a `banner` that assigns it to `globalThis`. Its `config` is the WHOLE resolved config, because the main process boots from it in a packaged app, and both bundles are Node rather than a public surface. `process.env.NODE_ENV` is defined the same way: webpack derived it from its `mode`, esbuild has no modes, so the build states it.
105
- - `composeClientBuildJson()` → the renderer, written ONCE as `dist/build.js` through `@omega.js/devkit/build-json` ([#743](https://github.com/Omega-JS-Stack/omega/issues/743)). Its `config` is `clientConfig(resolved)` from `@omega.js/config`, the browser-safe subset every OMEGA browser surface carries: a renderer is readable from DevTools, so the GCP account facts, the signing certificates and the account admins stay out of it. The page template loads the file with `<script src="../../build.js">` as the view's FIRST script, ahead of the view bundle (`dist/views/<view>/` → `dist/`, resolved inside a packaged asar exactly as the bundle tag beside it is), and the renderer bundle carries no define and no banner of its own.
106
-
107
- The build facts ride on both: `runtime: 'electron'` ([#896](https://github.com/Omega-JS-Stack/omega/issues/896), the fact @omega.js/client cannot sniff from inside a renderer), `environment`, `version`, `buildTime`, `target`, and the resolved `dev` map on non-production builds.
108
-
109
- Pinned by `src/test/suites/build/build-json-bake.test.js`.
110
-
111
- ## electron-builder
112
-
113
- @omega.js/desktop **generates** `dist/electron-builder.yml` from `config/omega.json5` + @omega.js/desktop defaults — the consumer never ships an `electron-builder.yml`. `gulp/build-config` does the materialization, applying:
114
-
115
- - App metadata: `appId`, `productName`, `copyright` (with `{YEAR}` token expansion to the current year)
116
- - App-level cross-platform fields: `category` mapping, `languages`, `darkModeSupport`
117
- - Per-target (per-platform) installer config from `targets.{mac,win,linux}`:
118
- - **mac**: arch (default `universal`), MAS stubs (not implemented)
119
- - **win**: arch (default `x64`+`ia32`), NSIS oneClick + shortcuts
120
- - **linux**: arch, optional snap publishing
121
- - Target lists derived from the `platforms` declaration and versionless `artifactName` templates from `@omega.js/config`'s `platforms.js`: the ONE format table the website's direct-download URLs read too, so `/releases/latest/download/<asset>` never changes. The asset table + the whole release contract: [releasing.md](releasing.md#versionless-assets-and-direct-download-links)
122
- - Mode-dependent injections like `mac.extendInfo.LSUIElement: true` when `startup.mode === 'hidden'` (zero-bounce production launches — see [startup.md](startup.md))
123
- - `electronVersion` pinned from the INSTALLED electron (resolved via the framework's module context — electron-builder refuses semver ranges and can't see a workspace-hoisted electron from the target dir)
124
- - Generated entitlements + resolved icons + materialized publish + afterSign hook. The publish block is CONFIG-ONLY and fully derived ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)): `<brand.id>-releases` under `repo.org`, through @omega.js/config's `releasesRepo`. No git-remote discovery and no typed repo name (a brand-monorepo target's remote is the repo it is nested in; electron-builder's update-info step crashes on a null publish config, so this isn't cosmetic)
125
- - Optional passthrough: `fileAssociations`, `protocols`
126
- - The `files` list: everything under the target root except source maps, `.env` files, `logs/`, and the scratch and state dirs `.omega/`, `.claude/`, `.temp/`, `.cache/`, `.gh-runners/` and `test/` ([#866](https://github.com/Omega-JS-Stack/omega/issues/866): the boot runner stages `.omega/test-app` with symlinks into the target, and the packager followed them). `src/` ships, because the runtime reads `src/integrations/*` from the app root; `config/` (the build resources dir) and `release/` are excluded by electron-builder itself
127
-
128
- The full per-target reference (every config knob, default value, and what it produces in YAML) lives in **[installer-options.md](installer-options.md)**.
129
-
130
- `gulp/package` and `gulp/package-quick` both point electron-builder at the generated `dist/electron-builder.yml`. Consumer overrides via `config.electronBuilder.*` are merged on top of the generated config — see [installer-options.md § Raw `electronBuilder` overrides](installer-options.md#raw-electronbuilder-overrides-escape-hatch) for the escape hatch.
131
-
132
- ## Build modes
133
-
134
- Environment variables (set in-process by the `omega build` / `omega package` / `omega publish` verbs — the consumer's npm scripts are thin `npx omega` aliases):
135
-
136
- | Var | Effect |
137
- |---|---|
138
- | `OMEGA_BUILD_MODE=true` | Production bundles (minified, name-mangled, no sourcemaps, `@dev-only` blocks stripped) |
139
- | `OMEGA_BUILD_OUTPUT=<path>` | The boot-test seam: redirect the gulp BUILD output away from `<project>/dist` (absolute, or relative to the project root). Resolved by [src/utils/dist-root.js](../src/utils/dist-root.js), which every build task's output path goes through — but `omega clean` and the generated `electron-builder.yml` stay project-relative, so this is NOT a general relocation switch; packaging under it is unsupported. Used by the boot-test runner so a test build never collides with the `npm start` watcher's `dist/` ([test-boot-layer.md](test-boot-layer.md#isolated-build-output)) |
140
- | `OMEGA_IS_PUBLISH=true` | electron-builder runs with `--publish always` |
141
- | `OMEGA_IS_SERVER=true` | Running in CI |
142
-
143
- ## Windows code signing
144
-
145
- Strategy-pluggable via `platforms.windows.signing.strategy` in `config/omega.json5`:
146
-
147
- | Strategy | Where signing runs | When to use |
148
- |---|---|---|
149
- | `self-hosted` | Self-hosted GH Actions runner with USB EV token plugged in | Default for @omega.js/desktop v1 — physical EV token desktop |
150
- | `cloud` | `windows-latest` runner shells out to a cloud signing CLI (Azure Trusted Signing / SSL.com / DigiCert KeyLocker) | Future migration target |
151
- | `local` | Developer's Windows machine after CI uploads unsigned artifact | Fallback when no runner is available |
152
-
153
- The `gulp/build-config` task and `electron-builder.yml`'s `win.sign` hook both honor `platforms.windows.signing.strategy` so the same code path drives all three. Provider modules live in `src/lib/sign-providers/{ev,azure,sslcom,digicert}.js` (Pass 3).
154
-
155
- ## GitHub Actions
156
-
157
- `.github/workflows/build.yml` (in `src/defaults/`) runs a 3-OS matrix: macOS / Linux / Windows. Windows job uploads unsigned; a separate `windows-sign` job runs on the strategy-appropriate runner and attaches signed artifacts to the release.
158
-
159
- Env vars set globally:
160
-
161
- ```yaml
162
- NODE_VERSION: '22'
163
- OMEGA_BUILD_MODE: 'true'
164
- OMEGA_IS_PUBLISH: 'true'
165
- OMEGA_IS_SERVER: 'true'
166
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
167
- ```
168
-
169
- Concurrency group: `${{ github.ref }}` with `cancel-in-progress`.
@@ -1,169 +0,0 @@
1
- # CDP Debugging (Claude ↔ Electron)
2
-
3
- @omega.js/desktop's `serve` task forwards all `--` CLI flags to the Electron child process. This enables Chrome DevTools Protocol (CDP) debugging, which lets Claude (or any CDP client) interact with the running Electron app — take screenshots, click elements, type text, evaluate JS, read console logs, inspect network requests, etc.
4
-
5
- Two ways to use it: **the built-in `npx omega cdp` toolkit** (below — zero setup, multi-target, knows @omega.js/desktop's conventions) and the `chrome-devtools-electron` MCP (further down — richer single-page interaction: click/fill/network/traces).
6
-
7
- ## Launching with CDP
8
-
9
- Two equivalent ways:
10
-
11
- ```bash
12
- # Env var (recommended)
13
- OMEGA_CDP_PORT=9222 npm start
14
-
15
- # CLI flag
16
- npm start -- --remote-debugging-port=9222
17
- ```
18
-
19
- Both add `--remote-debugging-port=9222` to the Electron spawn args. The env var takes precedence if both are set; a CLI flag already present won't be duplicated.
20
-
21
- Verify CDP is live:
22
-
23
- ```bash
24
- curl -s http://localhost:9222/json
25
- # Returns JSON array of page targets (one per BrowserWindow)
26
- ```
27
-
28
- ## The `mgr cdp` toolkit
29
-
30
- Zero-dependency subcommands for driving the running dev app — see, act, and run the no-watch iterate loop. All read `OMEGA_CDP_PORT` (default 9222), or take `--port <n>` (which wins).
31
-
32
- **Pinning a port in an npm script? Use `--port`, never `cross-env`.** `npx cross-env OMEGA_CDP_PORT=… npx omega cdp eval … "window.desktop.ipc.invoke('my:channel')"` STRIPS the inner quotes from the expression before it reaches V8 (`invoke(my:channel)` → `SyntaxError: missing ) after argument list`). `npx omega cdp eval … "…" --port <n>` keeps the expression intact.
33
-
34
- ```bash
35
- npx omega cdp status # running? targets, window rect, theme
36
- npx omega cdp eval <match> '<expr>' # evaluate JS in any webContents
37
- npx omega cdp shot <match> <out.png> # ONE renderer's own pixels
38
- npx omega cdp capture <out.png> # the COMPOSITED window (macOS)
39
- npx omega cdp theme <dark|light|system> # flip the live theme (omega.theme)
40
- npx omega cdp relaunch # quit → npm start → wait for boot
41
- npx omega cdp quit # quit + wait for the process tree to drain
42
- ```
43
-
44
- ### The multi-target model
45
-
46
- An @omega.js/desktop app is one window but potentially MANY webContents (every BrowserWindow + every `WebContentsView` is its own CDP page target). Every subcommand takes a **URL-substring matcher** instead of a "current page": the main window's document is always `/views/main/` (@omega.js/desktop's templating convention); other views match by their own URLs. `status` lists what's live.
47
-
48
- ```bash
49
- npx omega cdp eval "/views/main/" 'document.title'
50
- npx omega cdp eval "example.com" 'getComputedStyle(document.body).backgroundColor'
51
- npx omega cdp eval "/views/main/" "window.desktop.ipc.invoke('my-app:some-channel')" # the real IPC surface
52
- ```
53
-
54
- Promises are awaited, results print as JSON, and expressions run with a user gesture (focus()/clipboard-ish APIs behave like real input).
55
-
56
- ### shot vs capture — the compositing discriminator
57
-
58
- `shot` asks ONE renderer for its own surface; `capture` photographs the window the OS composited (the BrowserWindow document + every WebContentsView stacked). They answer different questions:
59
-
60
- - Styling wrong in one view? → `shot` that target.
61
- - Layering/transparency/z-order wrong? → `capture`; if `shot` looks right but `capture` doesn't, the bug is in compositing, not your CSS.
62
-
63
- Caveats:
64
- - `shot` needs a VISIBLE surface — a hidden view (`setVisible(false)`, a background view) produces no compositor frames and the capture times out. Show/select it first.
65
- - `capture` raises the app and region-captures its rect — anything overlaying that region still wins. For an occlusion-proof image: `--find-window-id` (slow Swift path; JXA's CoreGraphics bridge segfaults) → `capture <out.png> --window-id <id>` (the ID is stable per window lifetime — cache it).
66
- - **Color profiles:** macOS embeds the MONITOR's ICC profile in screenshot PNGs; many viewers (including image previews in tooling) misrender it dramatically — an opaque dark panel can read near-white. `capture` converts every file to sRGB via `sips`, so it's portable. Hand-rolled screencaptures should do the same: `sips -m "/System/Library/ColorSync/Profiles/sRGB Profile.icc" shot.png --out shot.png`.
67
- - `capture`, `relaunch`, and `quit` are macOS-only (screencapture / sips / osascript). `status`/`eval`/`shot`/`theme` are cross-platform.
68
-
69
- ### relaunch / quit — the iterate loop
70
-
71
- @omega.js/desktop dev has **no watch** (`npm start` builds once, then runs) — every `src/` edit needs quit → rebuild → boot. `relaunch` is that loop in one command: it quits the app (real quit — `before-quit` handlers run), waits for the **full process tree to drain** (port-down alone is NOT that signal — the npm-start chain takes a few more seconds, and a test run started inside that window gets contaminated with flaky boot suites), spawns a detached `npm start` with `OMEGA_CDP_PORT`, and waits for the boot signal. `quit` is the first half alone — safe to run `npx omega test` the moment it returns. **Never run tests while the app is going down** (a test run started before the process tree drains gets contaminated) — boot tests build into their own `.omega/test-app/` output since #110, so `dist/` itself no longer collides.
72
-
73
- The boot signal defaults to the main window's document target. Apps whose boot completes later than first paint override it in `config/omega.json5`:
74
-
75
- ```json5
76
- cdp: {
77
- readySignal: 'my-app://overlay', // URL substring of the target that appears LAST in boot
78
- }
79
- ```
80
-
81
- The packaged-app process name (for quit/raise/window-id matching) comes from config too: `app.productName` (derived from `brand.name`); dev builds run under "Electron".
82
-
83
- ## Driving a regular Chrome (not the app)
84
-
85
- Sometimes the thing to drive is a regular **Chrome** — the marketing site, a web flow, an OAuth page — not the Electron app.
86
-
87
- > Mirrored across the five sister frameworks (UJM / @omega.js/backend / BXM / @omega.js/desktop / @omega.js/client) — same core section, framework-flavored. Edit all five together.
88
-
89
- Browser work runs through the **`chrome-devtools` MCP** (via mcp-router). There is NO launch procedure anymore — no ports, no profile dirs, no curl checks:
90
-
91
- - **Just call the tools** — `new_page`, `navigate_page`, `take_screenshot`, `click`, `fill`, `evaluate_script`, `list_console_messages`, `list_network_requests`. The browser auto-launches on the first call.
92
- - **Each Claude session gets its OWN private Chrome** (`--isolated`): temp profile, CDP over an internal pipe. Parallel sessions cannot see or touch each other's pages — open and close pages freely, the whole browser is yours.
93
- - **It dies with the session.** No orphans, no cleanup, nothing to kill.
94
- - **Ephemeral profile** — cookies/logins do NOT persist between sessions. If a flow needs auth, log in during the task.
95
- - **Self-signed HTTPS is pre-accepted** (`--acceptInsecureCerts` in the upstream) — dev servers load without certificate interstitials.
96
- - **NEVER quit/kill Chrome by app name** (`killall "Google Chrome"`, osascript) — that's the user's personal browser, not yours.
97
-
98
- Humans: the agent's Chrome window is visible — you can watch it drive. Full reference: `~/.claude/mcp-server/servers/chrome-devtools/CLAUDE.md`.
99
-
100
- @omega.js/desktop specifics:
101
-
102
- - **The Electron app stays attach-by-port** — that's the whole rest of this doc (`mgr cdp` per invocation, or the `chrome-devtools-electron` MCP below). Port convention: **9222** = the Electron app. The isolated `chrome-devtools` browser has nothing to do with the app.
103
- - **Navigating to a brand's UJM dev site (the local marketing site)?** **`https://localhost:4000` — NEVER the LAN IP** (`https://192.168.x.x:...`). Port 4000 by default, increments to 4001+ when multiple sites run; exact port in `.temp/_config_browsersync.yml` at the root of the WEBSITE project (the UJM consumer — e.g. `<brand>-website/.temp/_config_browsersync.yml`, NOT this app repo).
104
-
105
- ## What CDP exposes
106
-
107
- - **Renderer processes only** — one "page" target per BrowserWindow. Main process is NOT exposed (use `--inspect=9229` for that, which is a separate V8 inspector protocol).
108
- - Each BrowserWindow appears as a separate page target. MCP tools with `list_pages`/`select_page` can switch between them.
109
- - Chromium silently ignores flags it doesn't recognize, so gulp's own flags (`--cwd`, `--gulpfile`) pass through harmlessly.
110
-
111
- ## MCP setup (Claude ↔ Electron)
112
-
113
- A `chrome-devtools-electron` MCP upstream is configured at `~/.claude/mcp-server/servers/chrome-devtools-electron/config.json`. It reads `OMEGA_CDP_PORT` from the environment at spawn time (defaults to 9222), so the same upstream works for any Electron app — just set the env var **BEFORE launching `claude`** (it's expanded once, when the session's router spawns the upstream; mid-session changes do nothing).
114
-
115
- ```json
116
- {
117
- "enabled": true,
118
- "command": "sh",
119
- "args": ["-c", "exec /Users/ian/.nvm/default-bin/npx -y chrome-devtools-mcp@latest --browserUrl=http://127.0.0.1:${OMEGA_CDP_PORT:-9222} --usage-statistics=false"]
120
- }
121
- ```
122
-
123
- This runs alongside the regular `chrome-devtools` upstream (which launches its own per-session isolated browser — no env vars, no ports). Tools are namespaced:
124
- - `chrome-devtools__take_screenshot` → the session's own Chrome browser
125
- - `chrome-devtools-electron__take_screenshot` → the running Electron app
126
-
127
- Multiple Claude sessions can debug different apps simultaneously — each terminal sets its own `OMEGA_CDP_PORT`. See `~/.claude/mcp-server/README.md`.
128
-
129
- ### Available MCP tools (29)
130
-
131
- Screenshots, click, fill, type, hover, drag, evaluate JS, list/read console messages, list/inspect network requests, navigate, resize, keyboard input, accessibility snapshots, Lighthouse audits, performance traces, heap snapshots, dialog handling.
132
-
133
- ### Session setup
134
-
135
- If the upstream was added/enabled after a Claude session started, its tools won't appear until next session. To enable mid-session:
136
-
137
- ```
138
- # From shell
139
- mcp enable chrome-devtools-electron
140
-
141
- # From inside Claude (if upstream is enabled on disk but not in session)
142
- router__enable_upstream { name: "chrome-devtools-electron" }
143
- ```
144
-
145
- ## Port conventions
146
-
147
- | Port | Protocol | Usage |
148
- |------|----------|-------|
149
- | 9222 | CDP (HTTP + WebSocket) | Electron renderer debugging (standard CDP port) |
150
- | 9229 | V8 Inspector | Node.js / Electron main process debugging (`--inspect`) |
151
-
152
- Use 9222 for `--remote-debugging-port` (industry standard). Avoid 9229 — that's the Node.js inspector port and a different protocol.
153
-
154
- ## Security
155
-
156
- CDP gives full control of the renderer — any local process can connect and read/modify anything. **Never ship with `--remote-debugging-port` baked in.** It's dev-only, gated behind `OMEGA_CDP_PORT` which is never set in production.
157
-
158
- ## How it works
159
-
160
- `src/gulp/tasks/serve.js` collects extra args in two ways:
161
-
162
- 1. **CLI flags**: `process.argv.slice(2).filter(arg => arg.startsWith('--'))` — forwards all `--` flags from the gulp process to Electron
163
- 2. **`OMEGA_CDP_PORT` env var**: if set and no `--remote-debugging-port` is already in the args, appends `--remote-debugging-port=${OMEGA_CDP_PORT}`
164
-
165
- The args are passed to `spawn(electronBin, ['.', ...extraArgs])`. The main process boot log shows the received argv:
166
-
167
- ```
168
- [info] (main) Initializing @omega.js/desktop (main)... argv=[".","--remote-debugging-port=9222"]
169
- ```