@omega.js/desktop 0.50.0 → 0.51.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 (267) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/css/tokens/_index.scss +1 -1
  3. package/dist/assets/themes/base/_includes/frontend/sections/account-section-header.html +4 -1
  4. package/dist/assets/themes/base/_includes/frontend/sections/footer.html +13 -6
  5. package/dist/assets/themes/base/_includes/frontend/sections/nav.html +10 -8
  6. package/dist/assets/themes/base/_includes/global/sections/account.html +3 -1
  7. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +12 -8
  8. package/dist/assets/themes/base/_includes/global/sections/app-topbar.html +10 -6
  9. package/dist/assets/themes/base/_includes/global/sections/page-header.html +8 -4
  10. package/dist/assets/themes/base/_layouts/backend/pages/dashboard/index.html +24 -24
  11. package/dist/assets/themes/base/_layouts/frontend/pages/about.html +10 -10
  12. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +35 -35
  13. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +4 -4
  14. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signin.html +1 -1
  15. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signup.html +4 -4
  16. package/dist/assets/themes/base/_layouts/frontend/pages/contact.html +3 -3
  17. package/dist/assets/themes/base/_layouts/frontend/pages/download.html +34 -33
  18. package/dist/assets/themes/base/_layouts/frontend/pages/extension/index.html +11 -11
  19. package/dist/assets/themes/base/_layouts/frontend/pages/legal/document.html +1 -1
  20. package/dist/assets/themes/base/_layouts/frontend/pages/status.html +1 -1
  21. package/dist/assets/themes/base/_layouts/frontend/pages/team/index.html +9 -7
  22. package/dist/assets/themes/base/_layouts/frontend/pages/team/member.html +5 -3
  23. package/dist/assets/themes/base/_sections/about/letter/section.html +1 -1
  24. package/dist/assets/themes/base/_sections/about/letter/section.json5 +4 -4
  25. package/dist/assets/themes/base/_sections/marketing/bento/section.html +1 -1
  26. package/dist/assets/themes/base/_sections/marketing/bento/section.json5 +15 -15
  27. package/dist/assets/themes/base/_sections/marketing/cta/section.json5 +1 -1
  28. package/dist/assets/themes/base/_sections/marketing/hero/section.html +6 -6
  29. package/dist/assets/themes/base/_sections/marketing/hero/section.json5 +10 -10
  30. package/dist/assets/themes/base/_sections/marketing/product-demo/section.html +1 -1
  31. package/dist/assets/themes/base/_sections/marketing/product-demo/section.json5 +2 -2
  32. package/dist/assets/themes/base/_sections/marketing/stats/section.html +1 -1
  33. package/dist/assets/themes/base/_sections/marketing/stats/section.json5 +5 -5
  34. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.html +1 -1
  35. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.json5 +2 -2
  36. package/dist/assets/themes/neobrutalism/_layouts/frontend/pages/index.html +10 -10
  37. package/dist/assets/themes/newsflash/_layouts/frontend/pages/index.html +10 -10
  38. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.html +2 -2
  39. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.json5 +4 -4
  40. package/dist/build.js +69 -28
  41. package/dist/cli-run.js +20 -13
  42. package/dist/cli.js +12 -7
  43. package/dist/commands/build.js +0 -2
  44. package/dist/commands/deploy.js +116 -44
  45. package/dist/commands/finalize-release.js +5 -4
  46. package/dist/commands/launch.js +12 -7
  47. package/dist/commands/lib/deploy-precheck.js +66 -34
  48. package/dist/commands/lib/ensure-target.js +71 -11
  49. package/dist/commands/package.js +0 -1
  50. package/dist/commands/publish.js +11 -4
  51. package/dist/commands/release.js +99 -252
  52. package/dist/commands/runner.js +42 -5
  53. package/dist/commands/sign-windows.js +46 -15
  54. package/dist/commands/test.js +4 -3
  55. package/dist/commands/validate-certs.js +291 -115
  56. package/dist/config/page-template.html +4 -0
  57. package/dist/defaults/.github/workflows/build.yml +144 -64
  58. package/dist/defaults/AGENTS.md +3 -2
  59. package/dist/defaults/_.gitignore +4 -2
  60. package/dist/defaults/config/certs/README.md +25 -45
  61. package/dist/defaults/config/omega.json5 +66 -32
  62. package/dist/defaults/hooks/deploy/pre.js +10 -0
  63. package/dist/defaults/src/assets/scss/main.scss +12 -2
  64. package/dist/defaults/src/integrations/tray/index.js +1 -1
  65. package/dist/gulp/main.js +13 -25
  66. package/dist/gulp/tasks/audit.js +18 -6
  67. package/dist/gulp/tasks/build-config.js +181 -60
  68. package/dist/gulp/tasks/bundle.js +108 -36
  69. package/dist/gulp/tasks/release.js +86 -4
  70. package/dist/gulp/tasks/sass.js +11 -0
  71. package/dist/hooks/lib/notarize-tools.js +137 -0
  72. package/dist/hooks/notarize-artifacts.js +57 -0
  73. package/dist/hooks/notarize.js +59 -8
  74. package/dist/lib/auth-persistence.js +25 -0
  75. package/dist/lib/client-bridge.js +11 -8
  76. package/dist/lib/deep-link.js +15 -8
  77. package/dist/lib/protocol.js +7 -1
  78. package/dist/lib/restart-manager/install.js +6 -3
  79. package/dist/lib/sign-helpers/auto-unlock.js +65 -27
  80. package/dist/lib/sign-helpers/console-lock.js +34 -0
  81. package/dist/lib/sign-helpers/exec-with-limit.js +68 -0
  82. package/dist/lib/sign-helpers/resolve-icons.js +6 -4
  83. package/dist/lib/tray.js +7 -6
  84. package/dist/main.js +16 -0
  85. package/dist/preload.js +8 -0
  86. package/dist/renderer.js +16 -4
  87. package/dist/runner/job-started.js +104 -0
  88. package/dist/test/fixtures/consumer-app/config/omega.json5 +7 -0
  89. package/dist/test/fixtures/consumer-app/package.json +1 -1
  90. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +7 -1
  91. package/dist/test/harness/main-entry.js +2 -1
  92. package/dist/test/harness/renderer-preload.js +12 -3
  93. package/dist/test/runners/boot.js +101 -11
  94. package/dist/test/runners/electron.js +1 -1
  95. package/dist/test/suites/boot/consumer-app-boots.test.js +54 -0
  96. package/dist/test/suites/build/audit.test.js +45 -6
  97. package/dist/test/suites/build/auth-persistence-resolve.test.js +119 -0
  98. package/dist/test/suites/build/auto-unlock.test.js +105 -0
  99. package/dist/test/suites/build/boot-runner-timeout.test.js +253 -0
  100. package/dist/test/suites/build/brand-scss.test.js +106 -0
  101. package/dist/test/suites/build/build-config.test.js +146 -36
  102. package/dist/test/suites/build/build-json-bake.test.js +245 -0
  103. package/dist/test/suites/build/build-verbs.test.js +48 -15
  104. package/dist/test/suites/build/build-workflow-jobs.test.js +190 -0
  105. package/dist/test/suites/build/cli.test.js +32 -10
  106. package/dist/test/suites/build/config-schema.test.js +4 -4
  107. package/dist/test/suites/build/console-lock.test.js +51 -0
  108. package/dist/test/suites/build/defaults-scaffold.test.js +71 -2
  109. package/dist/test/suites/build/deploy-direct.test.js +241 -0
  110. package/dist/test/suites/build/deploy-dispatch.test.js +287 -0
  111. package/dist/test/suites/build/deploy-hook.test.js +169 -0
  112. package/dist/test/suites/build/ensure-target.test.js +14 -2
  113. package/dist/test/suites/build/env-delivery.test.js +19 -10
  114. package/dist/test/suites/build/env-watch.test.js +18 -7
  115. package/dist/test/suites/build/esm-only-dependency.test.js +127 -0
  116. package/dist/test/suites/build/exec-with-limit.test.js +53 -0
  117. package/dist/test/suites/build/finalize-release.test.js +2 -2
  118. package/dist/test/suites/build/get-config.test.js +120 -8
  119. package/dist/test/suites/build/github-utils.test.js +12 -6
  120. package/dist/test/suites/build/license-stamp.test.js +6 -4
  121. package/dist/test/suites/build/manager.test.js +88 -68
  122. package/dist/test/suites/build/manifest-deps.test.js +116 -0
  123. package/dist/test/suites/build/merge-line-files.test.js +27 -4
  124. package/dist/test/suites/build/notarize-artifacts.test.js +135 -0
  125. package/dist/test/suites/build/notarize-tools.test.js +38 -0
  126. package/dist/test/suites/build/notarize.test.js +207 -0
  127. package/dist/test/suites/build/release-pipeline.test.js +38 -60
  128. package/dist/test/suites/build/release-skipped-upload.test.js +107 -0
  129. package/dist/test/suites/build/resolve-icons.test.js +35 -35
  130. package/dist/test/suites/build/runner-job-guard.test.js +182 -0
  131. package/dist/test/suites/build/runner.test.js +25 -2
  132. package/dist/test/suites/build/sentry.test.js +8 -3
  133. package/dist/test/suites/build/setup-scripts.test.js +3 -0
  134. package/dist/test/suites/build/sign-windows.test.js +92 -8
  135. package/dist/test/suites/build/test-stealth.test.js +17 -11
  136. package/dist/test/suites/build/url-helpers.test.js +37 -17
  137. package/dist/test/suites/build/validate-certs.test.js +428 -55
  138. package/dist/test/suites/build/validate-config.test.js +3 -3
  139. package/dist/test/suites/main/auth-flow.test.js +12 -0
  140. package/dist/test/suites/main/auth-persistence.test.js +14 -17
  141. package/dist/test/suites/main/auto-updater.test.js +2 -2
  142. package/dist/test/suites/main/boot-sequence.test.js +1 -1
  143. package/dist/test/suites/main/client-bridge.integration.test.js +5 -82
  144. package/dist/test/suites/main/client-bridge.test.js +19 -6
  145. package/dist/test/suites/main/deep-link.test.js +54 -0
  146. package/dist/test/suites/main/startup-paths-and-ua.test.js +1 -1
  147. package/dist/test/suites/main/url-helpers.test.js +81 -72
  148. package/dist/test/suites/renderer/cross-context-helpers.test.js +19 -16
  149. package/dist/utils/build-pipeline.js +10 -10
  150. package/dist/utils/github.js +12 -51
  151. package/dist/utils/load-env.js +66 -0
  152. package/dist/utils/mode-helpers.js +43 -111
  153. package/dist/utils/platform.js +37 -0
  154. package/dist/utils/runner-env.js +2 -1
  155. package/dist/utils/runner-job-guard.js +149 -0
  156. package/dist/utils/ship-keys.js +52 -0
  157. package/dist/utils/test-stealth.js +5 -3
  158. package/dist/utils/url-helpers.js +33 -17
  159. package/dist/vendor/config/bundle-id.js +53 -0
  160. package/dist/vendor/config/client-config.js +141 -0
  161. package/dist/vendor/config/company.js +334 -15
  162. package/dist/vendor/config/dev-facts.js +48 -0
  163. package/dist/vendor/config/env-delivery.js +231 -9
  164. package/dist/vendor/config/env-retired.js +137 -0
  165. package/dist/vendor/config/env-rules.js +22 -3
  166. package/dist/vendor/config/env-schema.js +234 -117
  167. package/dist/vendor/config/env.js +55 -26
  168. package/dist/vendor/config/environment.js +189 -0
  169. package/dist/vendor/config/hooks.js +13 -11
  170. package/dist/vendor/config/index.js +121 -44
  171. package/dist/vendor/config/load.js +366 -115
  172. package/dist/vendor/config/merge.js +2 -2
  173. package/dist/vendor/config/order.js +3 -3
  174. package/dist/vendor/config/platforms.js +276 -0
  175. package/dist/vendor/config/repo.js +226 -104
  176. package/dist/vendor/config/retired-keys.js +232 -27
  177. package/dist/vendor/config/schema.js +525 -99
  178. package/dist/vendor/config/site-global.js +63 -53
  179. package/dist/vendor/config/targets.js +187 -0
  180. package/dist/vendor/config/validate.js +119 -59
  181. package/dist/vendor/devkit/argv.js +118 -0
  182. package/dist/vendor/devkit/attach-log-file.js +21 -13
  183. package/dist/vendor/devkit/brand-tokens.js +278 -0
  184. package/dist/vendor/devkit/brand-version.js +264 -0
  185. package/dist/vendor/devkit/build-json.js +91 -0
  186. package/dist/vendor/devkit/bundle.js +48 -0
  187. package/dist/vendor/devkit/certificate-expiry.js +108 -0
  188. package/dist/vendor/devkit/certs.js +16 -194
  189. package/dist/vendor/devkit/ci-workflows.js +124 -8
  190. package/dist/vendor/devkit/cli-router.js +3 -3
  191. package/dist/vendor/devkit/defaults-engine.js +69 -7
  192. package/dist/vendor/devkit/deploy-follow.js +297 -0
  193. package/dist/vendor/devkit/deploy-precheck.js +23 -8
  194. package/dist/vendor/devkit/deploy-record.js +11 -27
  195. package/dist/vendor/devkit/deploy-snapshot.js +661 -0
  196. package/dist/vendor/devkit/deploy.js +445 -75
  197. package/dist/vendor/devkit/git-auth.js +73 -0
  198. package/dist/vendor/devkit/git-remote.js +95 -0
  199. package/dist/vendor/devkit/github-repo.js +290 -0
  200. package/dist/vendor/devkit/local.js +47 -0
  201. package/dist/vendor/devkit/merge-line-files.js +23 -16
  202. package/dist/vendor/devkit/omega-bin.js +18 -3
  203. package/dist/vendor/devkit/pack-local.js +391 -0
  204. package/dist/vendor/devkit/preludes/index.js +120 -0
  205. package/dist/vendor/devkit/preludes/origin-heal.js +156 -0
  206. package/dist/vendor/devkit/service-account.js +43 -0
  207. package/dist/vendor/devkit/ship-plan.js +112 -0
  208. package/dist/vendor/devkit/signing-env.js +180 -0
  209. package/dist/vendor/devkit/signing-tree.js +92 -0
  210. package/dist/vendor/devkit/target-seams.js +142 -0
  211. package/dist/vendor/devkit/target-secrets.js +235 -50
  212. package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
  213. package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
  214. package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
  215. package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
  216. package/dist/vendor/monitoring/env.js +20 -10
  217. package/dist/vendor/monitoring/main.js +1 -1
  218. package/dist/vendor/monitoring/preload.js +1 -1
  219. package/dist/vendor/monitoring/renderer.js +1 -1
  220. package/docs/analytics.md +1 -1
  221. package/docs/auto-updater.md +5 -5
  222. package/docs/boot-sequence.md +1 -1
  223. package/docs/build-system.md +15 -7
  224. package/docs/client-bridge.md +9 -7
  225. package/docs/config-schema.md +4 -4
  226. package/docs/css.md +8 -2
  227. package/docs/deep-link.md +12 -4
  228. package/docs/environment-detection.md +32 -24
  229. package/docs/hooks.md +3 -1
  230. package/docs/icons.md +7 -7
  231. package/docs/index.md +61 -23
  232. package/docs/installer-options.md +24 -21
  233. package/docs/logging.md +5 -5
  234. package/docs/releasing.md +25 -17
  235. package/docs/runner.md +40 -5
  236. package/docs/shared/brands.md +12 -6
  237. package/docs/shared/breaking-changes.md +375 -21
  238. package/docs/shared/config.md +757 -199
  239. package/docs/shared/deploys.md +194 -91
  240. package/docs/shared/icons.md +18 -0
  241. package/docs/shared/local-dev.md +24 -6
  242. package/docs/shared/logging.md +9 -6
  243. package/docs/shared/monitoring.md +27 -13
  244. package/docs/shared/publishing.md +3 -3
  245. package/docs/shared/rulings.md +2 -2
  246. package/docs/shared/testing.md +1 -1
  247. package/docs/shared/theming.md +26 -1
  248. package/docs/shared/translation.md +49 -7
  249. package/docs/shared/updates.md +1 -1
  250. package/docs/signing.md +59 -33
  251. package/docs/test-framework.md +10 -5
  252. package/docs/themes.md +15 -1
  253. package/package.json +18 -12
  254. package/bin/omega-desktop +0 -2
  255. package/dist/commands/push-secrets.js +0 -141
  256. package/dist/test/suites/build/deliver-certs.test.js +0 -95
  257. package/dist/test/suites/build/derive-signing-env.test.js +0 -122
  258. package/dist/test/suites/build/push-secrets.test.js +0 -226
  259. package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
  260. package/dist/utils/deliver-certs.js +0 -69
  261. package/dist/utils/derive-signing-env.js +0 -56
  262. package/dist/utils/resolve-signing-cert.js +0 -175
  263. package/dist/vendor/config/desktop-artifacts.js +0 -110
  264. package/dist/vendor/config/instances.js +0 -208
  265. /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
  266. /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
  267. /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
@@ -0,0 +1,391 @@
1
+ /**
2
+ * pack-local: make a tree's `file:` dependencies installable somewhere else
3
+ * ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
4
+ *
5
+ * A remote install can only resolve what travels with it: a dependency spelled
6
+ * `file:../../../../packages/web` resolves to nothing on a runner or inside
7
+ * Cloud Build, and an install that runs `npm ci` also demands a
8
+ * package-lock.json matching the uploaded shape.
9
+ *
10
+ * This stages an INSTALL ROOT in place: packs each LOCAL package the install
11
+ * needs into `<dir>/omega_modules/*.tgz` (npm pack runs the package's own
12
+ * prepare, so the tarball always reflects current source), respells every
13
+ * manifest that names one, and regenerates the lockfile against that shape.
14
+ * `restore()` puts each touched manifest and the lockfile back verbatim and
15
+ * removes the tarball folder, and a FAILING stage restores before it throws,
16
+ * so a stopped deploy never leaves a half-staged tree.
17
+ *
18
+ * `dir` is any install root: a brand root with workspaces, a standalone target,
19
+ * or a backend's generated functions folder. What gets packed is three things,
20
+ * not one:
21
+ *
22
+ * 1. the outside-the-dir `file:` deps of `<dir>/package.json` AND of every
23
+ * workspace member manifest (npm resolves the whole workspace on any
24
+ * install, so one linked member breaks the install of a clean one). Each
25
+ * manifest's spec is respelled relative to ITSELF.
26
+ * 2. transitively, each packed package's own @omega.js runtime deps that
27
+ * resolve through a node_modules SYMLINK. @omega.js/client is a real
28
+ * runtime dependency of @omega.js/backend, never vendored and unpublished
29
+ * under the publish latch, so packing only the framework left the lock
30
+ * regen asking the registry for it (404, no deploy, #331). Those are
31
+ * redirected by an `overrides` entry on the ROOT manifest, npm honoring
32
+ * overrides at the install root only.
33
+ * 3. the overrides the ENCLOSING workspace root carries, when `dir` is itself
34
+ * inside a staged tree. A member installed on its own is its own install
35
+ * root, so an override it inherited has to travel with it.
36
+ *
37
+ * A spec that already points at a `.tgz` is COPIED, never packed again: that is
38
+ * the SECOND HOP, where the runner stages a target out of the snapshot it was
39
+ * handed and the sources are a machine away. It is also what makes the module
40
+ * idempotent, a staged dir spelling every local dep inside itself and therefore
41
+ * collecting nothing on a re-stage.
42
+ */
43
+
44
+ // Libraries
45
+ const fs = require('fs');
46
+ const path = require('path');
47
+ const jetpack = require('fs-jetpack');
48
+ const powertools = require('node-powertools');
49
+ const chalk = require('chalk').default;
50
+
51
+ // Constants
52
+ const STAGING_DIR = 'omega_modules';
53
+ const SCOPE = '@omega.js/';
54
+ const FILE_SPEC = 'file:';
55
+ const DEP_BLOCKS = ['dependencies', 'devDependencies'];
56
+
57
+ /**
58
+ * Is `target` inside `dir` (or the dir itself)?
59
+ * @param {string} dir - The install root.
60
+ * @param {string} target - An absolute path.
61
+ * @returns {boolean} True when the install already carries it.
62
+ */
63
+ function isInside(dir, target) {
64
+ return target === dir || (target + path.sep).startsWith(dir + path.sep);
65
+ }
66
+
67
+ /** A `file:` spec for a path, always POSIX-spelled (npm reads no backslashes). */
68
+ function fileSpec(from, target) {
69
+ return FILE_SPEC + path.relative(from, target).split(path.sep).join('/');
70
+ }
71
+
72
+ /**
73
+ * Resolve a LINKED local package: the first node_modules entry walking up from
74
+ * `fromDir` (mirroring Node resolution, since npm workspaces and `omega i local`
75
+ * hoist to the brand/monorepo root), and only when that entry is a SYMLINK. A
76
+ * symlink IS the local-era shape, and the registry may have no copy of what it
77
+ * points at; a real directory is a registry install the remote can fetch itself.
78
+ * @param {string} name - Package name.
79
+ * @param {string} fromDir - Directory whose manifest declares it.
80
+ * @returns {string|null} Absolute real path of the linked checkout, or null.
81
+ */
82
+ function resolveLinkedPackage(name, fromDir) {
83
+ let current = path.resolve(fromDir);
84
+
85
+ while (true) {
86
+ const candidate = path.join(current, 'node_modules', name);
87
+ const entry = jetpack.inspect(candidate);
88
+
89
+ if (entry) {
90
+ if (entry.type !== 'symlink') {
91
+ return null;
92
+ }
93
+ const real = fs.realpathSync(candidate);
94
+ return jetpack.exists(path.join(real, 'package.json')) ? real : null;
95
+ }
96
+
97
+ const parent = path.dirname(current);
98
+ if (parent === current) {
99
+ return null;
100
+ }
101
+ current = parent;
102
+ }
103
+ }
104
+
105
+ /** The workspace globs a manifest declares, in either supported spelling. */
106
+ function workspaceGlobs(pkg) {
107
+ const workspaces = (pkg || {}).workspaces;
108
+ if (!workspaces) {
109
+ return [];
110
+ }
111
+
112
+ return Array.isArray(workspaces) ? workspaces : (workspaces.packages || []);
113
+ }
114
+
115
+ /**
116
+ * Every manifest an install of `dir` resolves: the root's, then each workspace
117
+ * member's.
118
+ * @param {string} dir - The install root.
119
+ * @param {object} pkg - Its parsed package.json.
120
+ * @returns {Array<{ dir: string, file: string, pkg: object, root: boolean }>} The manifests.
121
+ */
122
+ function collectManifests(dir, pkg) {
123
+ const manifests = [{ dir, file: path.join(dir, 'package.json'), pkg, root: true }];
124
+
125
+ for (const pattern of workspaceGlobs(pkg)) {
126
+ for (const match of jetpack.find(dir, { matching: `${pattern}/package.json`, files: true }) || []) {
127
+ const file = path.resolve(match);
128
+ // An install never reads a manifest out of node_modules as a member
129
+ if (file.split(path.sep).includes('node_modules')) {
130
+ continue;
131
+ }
132
+
133
+ const member = jetpack.read(file, 'json');
134
+ if (member) {
135
+ manifests.push({ dir: path.dirname(file), file, pkg: member, root: false });
136
+ }
137
+ }
138
+ }
139
+
140
+ return manifests;
141
+ }
142
+
143
+ /**
144
+ * The nearest ancestor manifest declaring workspaces: the install root `dir`
145
+ * belonged to before it became one of its own, and therefore the home of the
146
+ * overrides it inherits.
147
+ * @param {string} dir - The install root being staged.
148
+ * @returns {{ dir: string, pkg: object }|null} The enclosing workspace root.
149
+ */
150
+ function enclosingWorkspaceRoot(dir) {
151
+ let current = path.dirname(path.resolve(dir));
152
+
153
+ while (true) {
154
+ const pkg = jetpack.read(path.join(current, 'package.json'), 'json');
155
+ if (pkg && workspaceGlobs(pkg).length) {
156
+ return { dir: current, pkg };
157
+ }
158
+
159
+ const parent = path.dirname(current);
160
+ if (parent === current) {
161
+ return null;
162
+ }
163
+ current = parent;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Every local package the install must carry, and where each one is named.
169
+ *
170
+ * Two shapes ride together: an ENTRY is one place a manifest names a package
171
+ * (respelled in place, or redirected by an override on the root), and a SOURCE
172
+ * is the package itself (packed or copied exactly once, however many entries
173
+ * name it).
174
+ *
175
+ * @param {object} options - Options.
176
+ * @param {string} options.dir - The install root being staged.
177
+ * @param {Array<object>} options.manifests - collectManifests() output.
178
+ * @returns {{ entries: Array<object>, sources: Array<{ name: string, from: string }> }}
179
+ * Sources in pack order: direct first, then the linked ones they pull in.
180
+ */
181
+ function collectTargets({ dir, manifests }) {
182
+ const entries = [];
183
+ const sources = [];
184
+ const claimed = new Set();
185
+ const rootManifest = manifests[0];
186
+
187
+ const add = (entry) => {
188
+ entries.push(entry);
189
+ if (!claimed.has(entry.name)) {
190
+ claimed.add(entry.name);
191
+ sources.push({ name: entry.name, from: entry.from });
192
+ }
193
+ };
194
+
195
+ // Direct: file: specs resolving OUTSIDE the dir, per manifest. A spec already
196
+ // pointing inside is what the install carries, so it stays as it is.
197
+ for (const manifest of manifests) {
198
+ for (const block of DEP_BLOCKS) {
199
+ for (const [name, spec] of Object.entries(manifest.pkg[block] || {})) {
200
+ if (!spec.startsWith(FILE_SPEC)) {
201
+ continue;
202
+ }
203
+
204
+ const from = path.resolve(manifest.dir, spec.slice(FILE_SPEC.length));
205
+ if (!isInside(dir, from)) {
206
+ add({ name, from, manifest, block });
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ // Inherited: the overrides of the workspace root this dir is a member of, the
213
+ // ones npm would have applied to it there (the second hop's linked client).
214
+ const enclosing = enclosingWorkspaceRoot(dir);
215
+ const inherited = enclosing ? enclosing.pkg.overrides || {} : {};
216
+ const ownOverrides = rootManifest.pkg.overrides || {};
217
+
218
+ for (const [name, spec] of Object.entries({ ...inherited, ...ownOverrides })) {
219
+ if (typeof spec !== 'string' || !spec.startsWith(FILE_SPEC) || claimed.has(name)) {
220
+ continue;
221
+ }
222
+
223
+ const base = ownOverrides[name] === spec ? rootManifest.dir : enclosing.dir;
224
+ const from = path.resolve(base, spec.slice(FILE_SPEC.length));
225
+ if (!isInside(dir, from)) {
226
+ add({ name, from, manifest: rootManifest, block: null });
227
+ }
228
+ }
229
+
230
+ // Transitive: each packed package's linked @omega.js runtime deps. A copied
231
+ // tarball has none to walk: whatever it needed was resolved a hop ago and
232
+ // rides along as an inherited override.
233
+ for (let index = 0; index < sources.length; index++) {
234
+ const source = sources[index];
235
+ if (source.from.endsWith('.tgz')) {
236
+ continue;
237
+ }
238
+
239
+ const manifest = jetpack.read(path.join(source.from, 'package.json'), 'json');
240
+ for (const name of Object.keys((manifest || {}).dependencies || {})) {
241
+ if (!name.startsWith(SCOPE) || claimed.has(name)) {
242
+ continue;
243
+ }
244
+
245
+ const from = resolveLinkedPackage(name, source.from);
246
+ if (!from) {
247
+ continue; // Registry-installed (or absent): npm resolves it the normal way
248
+ }
249
+
250
+ add({ name, from, manifest: rootManifest, block: null });
251
+ }
252
+ }
253
+
254
+ return { entries, sources };
255
+ }
256
+
257
+ /**
258
+ * Stage an install root's local packages into it.
259
+ *
260
+ * @param {object} options - Options.
261
+ * @param {string} options.dir - The install root: a brand root, a target, or a
262
+ * generated functions folder.
263
+ * @param {Function} [options.log] - One line per step (silent by default).
264
+ * @returns {Promise<{ staged: string[], restore: Function }>} The packed names,
265
+ * and the restore that puts the tree back exactly as it was found.
266
+ */
267
+ async function stageLocalPackages({ dir, log = () => {} }) {
268
+ const root = path.resolve(dir);
269
+ const lockPath = path.join(root, 'package-lock.json');
270
+ const stagingPath = path.join(root, STAGING_DIR);
271
+
272
+ const pkg = jetpack.read(path.join(root, 'package.json'), 'json');
273
+ if (!pkg) {
274
+ return { staged: [], restore: async () => {} };
275
+ }
276
+
277
+ const manifests = collectManifests(root, pkg);
278
+ const rootManifest = manifests[0];
279
+ const { entries, sources } = collectTargets({ dir: root, manifests });
280
+
281
+ if (!sources.length) {
282
+ return { staged: [], restore: async () => {} };
283
+ }
284
+
285
+ // Originals, restored verbatim after the deploy and after a failure
286
+ const touched = [...new Set(entries.map((entry) => entry.manifest))];
287
+ const originals = touched.map((manifest) => ({ file: manifest.file, contents: jetpack.read(manifest.file) }));
288
+ const originalLock = jetpack.read(lockPath);
289
+
290
+ const restore = async () => {
291
+ for (const original of originals) {
292
+ jetpack.write(original.file, original.contents);
293
+ }
294
+
295
+ if (originalLock === undefined) {
296
+ jetpack.remove(lockPath);
297
+ } else {
298
+ jetpack.write(lockPath, originalLock);
299
+ }
300
+
301
+ jetpack.remove(stagingPath);
302
+ };
303
+
304
+ try {
305
+ jetpack.remove(stagingPath);
306
+ jetpack.dir(stagingPath);
307
+
308
+ const tarballs = {};
309
+
310
+ for (const source of sources) {
311
+ // A `.tgz` was packed a hop ago, by the stage that produced the tree this
312
+ // one is staging out of: copying it keeps the artifact identical and asks
313
+ // nothing of sources that are a machine away.
314
+ if (source.from.endsWith('.tgz')) {
315
+ if (!jetpack.exists(source.from)) {
316
+ throw new Error(`Local dependency ${source.name} names a tarball that is not there: ${source.from}`);
317
+ }
318
+
319
+ const filename = path.basename(source.from);
320
+ jetpack.copy(source.from, path.join(stagingPath, filename));
321
+ tarballs[source.name] = filename;
322
+ log(` ${chalk.green('✓')} Copied ${chalk.cyan(source.name)} → ${STAGING_DIR}/${filename}`);
323
+ continue;
324
+ }
325
+
326
+ if (!jetpack.exists(path.join(source.from, 'package.json'))) {
327
+ throw new Error(`Local dependency ${source.name} has no package.json at ${source.from}`);
328
+ }
329
+
330
+ log(` ${chalk.dim('→')} Packing local package ${chalk.cyan(source.name)}...`);
331
+
332
+ const output = await powertools.execute(
333
+ `npm pack --pack-destination "${stagingPath}" --foreground-scripts=false`,
334
+ { log: false, config: { cwd: source.from } },
335
+ );
336
+
337
+ const filename = String(output).trim().split('\n').pop().trim();
338
+ if (!filename.endsWith('.tgz') || !jetpack.exists(path.join(stagingPath, filename))) {
339
+ throw new Error(`npm pack for ${source.name} did not produce a tarball (got: ${filename || 'nothing'})`);
340
+ }
341
+
342
+ tarballs[source.name] = filename;
343
+ log(` ${chalk.green('✓')} Staged ${chalk.cyan(source.name)} → ${STAGING_DIR}/${filename}`);
344
+ }
345
+
346
+ const overrides = {};
347
+
348
+ for (const entry of entries) {
349
+ const tarball = path.join(stagingPath, tarballs[entry.name]);
350
+
351
+ if (entry.block) {
352
+ // Relative to the manifest that names it: a member's spec climbs out of
353
+ // its own folder, the root's does not.
354
+ entry.manifest.pkg[entry.block][entry.name] = fileSpec(entry.manifest.dir, tarball);
355
+ } else {
356
+ // A packed package still declares its own REGISTRY spec internally
357
+ // (`@omega.js/client: ^0.1.0`), so only an override redirects that
358
+ // nested resolution to the artifact beside it.
359
+ overrides[entry.name] = fileSpec(root, tarball);
360
+ }
361
+ }
362
+
363
+ if (Object.keys(overrides).length > 0) {
364
+ rootManifest.pkg.overrides = Object.assign({}, rootManifest.pkg.overrides, overrides);
365
+ }
366
+
367
+ for (const manifest of touched) {
368
+ jetpack.write(manifest.file, `${JSON.stringify(manifest.pkg, null, 2)}\n`);
369
+ }
370
+
371
+ // A remote install runs `npm ci`, so the uploaded lockfile must match the
372
+ // staged shape. `--prefix` is what makes npm treat THIS dir as the install
373
+ // root: run inside a workspace it would otherwise climb to the workspace
374
+ // root and write that one's lockfile instead.
375
+ log(` ${chalk.dim('→')} Regenerating package-lock.json for the staged shape...`);
376
+ jetpack.remove(lockPath);
377
+ await powertools.execute(
378
+ `npm install --package-lock-only --ignore-scripts --no-audit --no-fund --prefix "${root}"`,
379
+ { log: false, config: { cwd: root } },
380
+ );
381
+ } catch (error) {
382
+ // Loud and clean: the deploy stops here, and the tree goes back exactly as
383
+ // it was rather than sitting half-staged.
384
+ await restore();
385
+ throw new Error(`Local-package staging failed, nothing deployed and ${path.basename(root)}/ restored:\n${error.message}`);
386
+ }
387
+
388
+ return { staged: sources.map((source) => source.name), restore };
389
+ }
390
+
391
+ module.exports = { stageLocalPackages, STAGING_DIR };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * runPreludes: the BOOT PRELUDE list every verb passes through
3
+ * ([#890](https://github.com/Omega-JS-Stack/omega/issues/890)).
4
+ *
5
+ * One seam already sits above every verb on all five CLIs: the `freshnessBoot`
6
+ * call that heals a stale locally-linked dist before anything runs. Anything
7
+ * else the whole CLI surface must settle first had nowhere to live, so it lived
8
+ * nowhere: a brand clone pointing at a redirected GitHub repo was checked by no
9
+ * verb at all. This is that place: the preludes run right after the freshness
10
+ * boot, in list order, before the verb.
11
+ *
12
+ * Each prelude is ONE file with one concern, exporting `{ name, verbs, run }`,
13
+ * and declares what it targets: `'all'`, one verb name, or a list of them. The
14
+ * runner filters by the verb about to run, as TYPED, before a framework's
15
+ * alias table resolves it, since the tables are the frameworks' and this runs
16
+ * above them, so an `'all'` prelude is the one that covers `-v` and a bare
17
+ * `omega` too.
18
+ *
19
+ * A prelude is CHEAP and NON-INTERACTIVE: local reads, at most one network
20
+ * call, never a prompt. Anything heavier is a manage op in its own service,
21
+ * where a walk reports it. A prelude that stops the run THROWS with one line of
22
+ * why, which this runner prints before exiting 1, the same presentation the
23
+ * freshness boot's loud stops use, so a refused boot reads the same whichever
24
+ * step refused it.
25
+ *
26
+ * Nothing here caches per process: a dispatcher hop that re-enters a `run()`
27
+ * runs the preludes again, and every prelude being idempotent, the second pass
28
+ * is a local read that finds its work already done.
29
+ */
30
+
31
+ // The list. Order is the boot order.
32
+ const PRELUDES = [
33
+ require('./origin-heal.js'),
34
+ ];
35
+
36
+ /**
37
+ * The verb a prelude list filters on: the first positional of the invocation.
38
+ * A flag there (`--version`) is no verb: the CLI is about to run its default
39
+ * command, which only an `'all'` prelude covers.
40
+ *
41
+ * @param {string} [verb] - The raw first argument.
42
+ * @returns {string|null} The verb, or null when the invocation names none.
43
+ */
44
+ function normalizeVerb(verb) {
45
+ return typeof verb === 'string' && verb.length > 0 && !verb.startsWith('-') ? verb : null;
46
+ }
47
+
48
+ /**
49
+ * Does this prelude target the verb about to run?
50
+ *
51
+ * @param {{ name: string, verbs: string|string[] }} prelude - The prelude.
52
+ * @param {string|null} verb - The normalized verb.
53
+ * @returns {boolean}
54
+ * @throws {Error} When the prelude declares no usable `verbs`. A list entry
55
+ * nobody can target is a programmer error, and a silent skip would make it
56
+ * look like a prelude that simply never applies.
57
+ */
58
+ function targetsVerb(prelude, verb) {
59
+ const { verbs } = prelude;
60
+ if (verbs === 'all') return true;
61
+
62
+ const list = typeof verbs === 'string' ? [verbs] : verbs;
63
+ if (!Array.isArray(list) || list.length === 0 || list.some((entry) => typeof entry !== 'string')) {
64
+ throw new Error(`Boot prelude "${prelude.name}" declares no verbs: \`verbs\` is 'all', one verb name, or a list of verb names.`);
65
+ }
66
+
67
+ return verb !== null && list.includes(verb);
68
+ }
69
+
70
+ /**
71
+ * Run every prelude the verb selects, in list order.
72
+ *
73
+ * @param {object} [options]
74
+ * @param {string} [options.verb] - The verb about to run (the CLI's first positional).
75
+ * @param {string} [options.targetDir] - The invocation's directory (default: cwd).
76
+ * @param {string|null} [options.brandRoot] - The brand root; resolved from `targetDir` when absent.
77
+ * @param {object} [options.config] - The composed omega config, when the caller already has it.
78
+ * @param {Array<{ name: string, verbs: string|string[], run: function }>} [options.preludes] - The list (default: PRELUDES).
79
+ * @returns {{ ran: string[] }} The preludes that ran, in the order they ran.
80
+ */
81
+ function runPreludes(options = {}) {
82
+ const preludes = options.preludes || PRELUDES;
83
+ const verb = normalizeVerb(options.verb);
84
+ const targetDir = options.targetDir || process.cwd();
85
+ const brandRoot = options.brandRoot === undefined ? resolveBrandRoot(targetDir) : options.brandRoot;
86
+
87
+ // Filtering first, and over the WHOLE list: a malformed `verbs` is raised
88
+ // before any prelude has run, not halfway through the boot.
89
+ const selected = preludes.filter((prelude) => targetsVerb(prelude, verb));
90
+ const ran = [];
91
+
92
+ for (const prelude of selected) {
93
+ try {
94
+ prelude.run({ verb, brandRoot, targetDir, config: options.config });
95
+ } catch (error) {
96
+ console.error(`omega: ${error.message}`);
97
+ process.exit(1);
98
+ }
99
+ ran.push(prelude.name);
100
+ }
101
+
102
+ return { ran };
103
+ }
104
+
105
+ /**
106
+ * The brand root of an invocation, or null outside a brand (this monorepo's own
107
+ * packages, a fresh directory). Required lazily: `@omega.js/config` is loaded by
108
+ * most verbs anyway, and a boot that resolves nothing should not pay for it.
109
+ *
110
+ * @param {string} targetDir - The invocation's directory.
111
+ * @returns {string|null}
112
+ */
113
+ function resolveBrandRoot(targetDir) {
114
+ return require('../../config/index.js').resolveBrandRoot(targetDir);
115
+ }
116
+
117
+ module.exports = {
118
+ runPreludes,
119
+ PRELUDES,
120
+ };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The origin heal: a brand whose `origin` names a repo GitHub has since
3
+ * redirected is pointed at the address GitHub answers with
4
+ * ([#890](https://github.com/Omega-JS-Stack/omega/issues/890)).
5
+ *
6
+ * A transfer or a rename leaves every clone pointing at the old address. Git
7
+ * follows GitHub's redirect and keeps working, so nothing ever complains, and
8
+ * the stale remote surfaces later as a push to a repo the brand no longer is
9
+ * (seen on the `omega-omega` transfer into `Omega-JS-Stack`, 2026-09-12).
10
+ *
11
+ * The REDIRECT is the authority, not the config: `gh api repos/<owner>/<name>`
12
+ * on the slug the checkout already carries follows GitHub's own 301 and answers
13
+ * the CURRENT `full_name`, so a brand is healed onto wherever its repo actually
14
+ * moved, config or no config. That costs one GitHub read per boot, which is the
15
+ * price of healing from the truth instead of from a typed value.
16
+ *
17
+ * The decision, in the order it is cheapest to answer:
18
+ * - no brand root: nothing to compare, and not one read made;
19
+ * - no `.git` AT the brand root: a no-op, never a walk up, and answered by one
20
+ * stat before anything else is read. Git resolves a repo by walking up, and
21
+ * a brand nested in another repo (a fixture brand inside this monorepo, a
22
+ * target checked out under someone else's tree) would otherwise have the
23
+ * ENCLOSING repo's remote read and rewritten;
24
+ * - no `origin`, or an origin that is not a GitHub remote: a no-op. A
25
+ * checkout nobody has pushed yet, and a brand hosted elsewhere, are
26
+ * expected external conditions, not failures;
27
+ * - a repo GitHub does not have (a 404: it is gone), or a GitHub it cannot
28
+ * reach (offline, no `gh` auth): a silent no-op. Rewriting a remote on an
29
+ * answer nobody got would take a working checkout and break it;
30
+ * - GitHub answering the slug the remote already names (compared
31
+ * case-insensitively, GitHub's own comparison): converged, silent;
32
+ * - anything else: the remote is rewritten to the answered `full_name`,
33
+ * keeping its own url form, and the heal is stated in one line.
34
+ *
35
+ * Then DRIFT, on whatever the answer was: `repo.org` stays the one typed value
36
+ * in `config/omega.json5`, and an owner GitHub reports that differs from it is
37
+ * stated in one line and nothing more. The prelude never writes config: which
38
+ * of the two is wrong (the config, or where the repo lives) is a human's call,
39
+ * and the manager's repo service only ensures repos UNDER `repo.org`.
40
+ *
41
+ * The WRITE is the one thing that fails loudly: a `git remote set-url` refused
42
+ * after a `get-url` just answered is not an external condition, it is a broken
43
+ * invariant, and the runner stops the boot on it.
44
+ */
45
+
46
+ const path = require('node:path');
47
+ const jetpack = require('fs-jetpack');
48
+
49
+ const { parseRemoteUrl, retargetRemoteUrl, remoteUrl, setRemoteUrl } = require('../git-remote.js');
50
+ const github = require('../github-repo.js');
51
+
52
+ // The one network read runs on EVERY verb boot, so a network that hangs (a
53
+ // captive portal, a dropped link) is bounded here and lands on the same silent
54
+ // `unreachable` branch an offline machine does.
55
+ const GITHUB_READ_TIMEOUT_MS = 10000;
56
+
57
+ /**
58
+ * @param {object} context - The prelude context.
59
+ * @param {string|null} context.brandRoot - The brand root of the invocation.
60
+ * @param {object} [context.config] - The composed omega config, when the caller has it.
61
+ * @param {function} [context.execFn] - Injectable git exec (tests).
62
+ * @param {function} [context.resolveRepo] - Injectable `(owner, name) => repo|null` (tests).
63
+ * @param {function} [context.log] - Injectable line printer (tests).
64
+ * @returns {{ healed: boolean, reason?: string, from?: string, to?: string, drift?: { owner: string, org: string } }}
65
+ */
66
+ function run(context = {}) {
67
+ const { brandRoot, execFn } = context;
68
+ const log = context.log || console.log;
69
+ // Called through the module so the ONE network read has a seam a real CLI
70
+ // boot can stub too (the integration test patches it here).
71
+ const resolveRepo = context.resolveRepo || ((owner, name) => github.getRepo(owner, name, { timeout: GITHUB_READ_TIMEOUT_MS }));
72
+
73
+ if (!brandRoot) return { healed: false, reason: 'no-brand' };
74
+
75
+ // One stat, before anything else: most boots in this monorepo's own trees and
76
+ // in every fixture brand end here.
77
+ if (!jetpack.exists(path.join(brandRoot, '.git'))) return { healed: false, reason: 'no-git' };
78
+
79
+ let url;
80
+ try {
81
+ url = remoteUrl({ dir: brandRoot, execFn });
82
+ } catch (e) {
83
+ return { healed: false, reason: 'no-origin' };
84
+ }
85
+
86
+ const current = parseRemoteUrl(url);
87
+ if (!current) return { healed: false, reason: 'foreign-remote' };
88
+
89
+ const from = `${current.owner}/${current.repo}`;
90
+
91
+ // The ONE network read, on the slug the checkout carries: GitHub follows its
92
+ // own redirect and names where that repo lives now.
93
+ let resolved;
94
+ try {
95
+ resolved = resolveRepo(current.owner, current.repo);
96
+ } catch (e) {
97
+ return { healed: false, reason: 'unreachable' };
98
+ }
99
+ if (!resolved) return { healed: false, reason: 'missing' };
100
+
101
+ const to = resolved.full_name;
102
+ let result;
103
+
104
+ if (to.toLowerCase() === from.toLowerCase()) {
105
+ result = { healed: false, reason: 'converged' };
106
+ } else {
107
+ setRemoteUrl({ dir: brandRoot, url: retargetRemoteUrl(url, to), execFn });
108
+ log(`omega: origin healed from ${from} to ${to}`);
109
+ result = { healed: true, from, to };
110
+ }
111
+
112
+ const drift = configDrift(brandRoot, context.config, to.split('/')[0]);
113
+ if (drift) {
114
+ log(`omega: origin lives under ${drift.owner} but repo.org is ${drift.org}: fix repo.org in config/omega.json5 or move the repo`);
115
+ result.drift = drift;
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ /**
122
+ * The gap between where the repo LIVES and the org the config types, or null
123
+ * when there is none to state. The config is loaded here when the caller has
124
+ * none, and an unloadable one answers "no drift": a config the boot cannot read
125
+ * is the VERB's failure to report, in its own words, never a prelude's. A brand
126
+ * declaring no `repo` block types no org, so there is nothing to disagree with.
127
+ *
128
+ * @param {string} brandRoot - The brand root.
129
+ * @param {object} [config] - The composed config, when the caller has it.
130
+ * @param {string} owner - The owner GitHub resolved the repo under.
131
+ * @returns {{ owner: string, org: string }|null}
132
+ */
133
+ function configDrift(brandRoot, config, owner) {
134
+ const { loadConfig, repoBlock } = require('../../config/index.js');
135
+
136
+ let block;
137
+ if (config) {
138
+ block = repoBlock(config);
139
+ } else {
140
+ try {
141
+ block = repoBlock(loadConfig(brandRoot).config);
142
+ } catch (e) {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ if (!block || block.org.toLowerCase() === owner.toLowerCase()) return null;
148
+
149
+ return { owner, org: block.org };
150
+ }
151
+
152
+ module.exports = {
153
+ name: 'origin-heal',
154
+ verbs: 'all',
155
+ run,
156
+ };