@omega.js/desktop 0.50.0 → 0.52.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 (268) hide show
  1. package/README.md +2 -2
  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 +176 -42
  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 +146 -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 +635 -5
  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 +125 -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 +249 -103
  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 +425 -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 +505 -75
  197. package/dist/vendor/devkit/git-auth.js +73 -0
  198. package/dist/vendor/devkit/git-remote.js +172 -0
  199. package/dist/vendor/devkit/github-repo.js +290 -0
  200. package/dist/vendor/devkit/local.js +98 -3
  201. package/dist/vendor/devkit/lockfile.js +127 -0
  202. package/dist/vendor/devkit/merge-line-files.js +23 -16
  203. package/dist/vendor/devkit/omega-bin.js +18 -3
  204. package/dist/vendor/devkit/pack-local.js +388 -0
  205. package/dist/vendor/devkit/preludes/index.js +120 -0
  206. package/dist/vendor/devkit/preludes/origin-heal.js +142 -0
  207. package/dist/vendor/devkit/service-account.js +43 -0
  208. package/dist/vendor/devkit/ship-plan.js +112 -0
  209. package/dist/vendor/devkit/signing-env.js +180 -0
  210. package/dist/vendor/devkit/signing-tree.js +92 -0
  211. package/dist/vendor/devkit/target-seams.js +142 -0
  212. package/dist/vendor/devkit/target-secrets.js +231 -57
  213. package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
  214. package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
  215. package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
  216. package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
  217. package/dist/vendor/monitoring/env.js +20 -10
  218. package/dist/vendor/monitoring/main.js +1 -1
  219. package/dist/vendor/monitoring/preload.js +1 -1
  220. package/dist/vendor/monitoring/renderer.js +1 -1
  221. package/docs/analytics.md +1 -1
  222. package/docs/auto-updater.md +5 -5
  223. package/docs/boot-sequence.md +1 -1
  224. package/docs/build-system.md +15 -7
  225. package/docs/client-bridge.md +9 -7
  226. package/docs/config-schema.md +4 -4
  227. package/docs/css.md +8 -2
  228. package/docs/deep-link.md +12 -4
  229. package/docs/environment-detection.md +32 -24
  230. package/docs/hooks.md +3 -1
  231. package/docs/icons.md +7 -7
  232. package/docs/index.md +61 -23
  233. package/docs/installer-options.md +24 -21
  234. package/docs/logging.md +5 -5
  235. package/docs/releasing.md +29 -19
  236. package/docs/runner.md +51 -14
  237. package/docs/shared/brands.md +12 -6
  238. package/docs/shared/breaking-changes.md +375 -21
  239. package/docs/shared/config.md +764 -199
  240. package/docs/shared/deploys.md +217 -91
  241. package/docs/shared/icons.md +18 -0
  242. package/docs/shared/local-dev.md +28 -8
  243. package/docs/shared/logging.md +9 -6
  244. package/docs/shared/monitoring.md +27 -13
  245. package/docs/shared/publishing.md +3 -3
  246. package/docs/shared/rulings.md +2 -2
  247. package/docs/shared/testing.md +1 -1
  248. package/docs/shared/theming.md +26 -1
  249. package/docs/shared/translation.md +49 -7
  250. package/docs/shared/updates.md +1 -1
  251. package/docs/signing.md +60 -34
  252. package/docs/test-framework.md +10 -5
  253. package/docs/themes.md +15 -1
  254. package/package.json +18 -12
  255. package/bin/omega-desktop +0 -2
  256. package/dist/commands/push-secrets.js +0 -141
  257. package/dist/test/suites/build/deliver-certs.test.js +0 -95
  258. package/dist/test/suites/build/derive-signing-env.test.js +0 -122
  259. package/dist/test/suites/build/push-secrets.test.js +0 -226
  260. package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
  261. package/dist/utils/deliver-certs.js +0 -69
  262. package/dist/utils/derive-signing-env.js +0 -56
  263. package/dist/utils/resolve-signing-cert.js +0 -175
  264. package/dist/vendor/config/desktop-artifacts.js +0 -110
  265. package/dist/vendor/config/instances.js +0 -208
  266. /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
  267. /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
  268. /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
@@ -0,0 +1,388 @@
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
+ const { regenerateLockfile } = require('./lockfile.js');
51
+
52
+ // Constants
53
+ const STAGING_DIR = 'omega_modules';
54
+ const SCOPE = '@omega.js/';
55
+ const FILE_SPEC = 'file:';
56
+ const DEP_BLOCKS = ['dependencies', 'devDependencies'];
57
+
58
+ /**
59
+ * Is `target` inside `dir` (or the dir itself)?
60
+ * @param {string} dir - The install root.
61
+ * @param {string} target - An absolute path.
62
+ * @returns {boolean} True when the install already carries it.
63
+ */
64
+ function isInside(dir, target) {
65
+ return target === dir || (target + path.sep).startsWith(dir + path.sep);
66
+ }
67
+
68
+ /** A `file:` spec for a path, always POSIX-spelled (npm reads no backslashes). */
69
+ function fileSpec(from, target) {
70
+ return FILE_SPEC + path.relative(from, target).split(path.sep).join('/');
71
+ }
72
+
73
+ /**
74
+ * Resolve a LINKED local package: the first node_modules entry walking up from
75
+ * `fromDir` (mirroring Node resolution, since npm workspaces and `omega i local`
76
+ * hoist to the brand/monorepo root), and only when that entry is a SYMLINK. A
77
+ * symlink IS the local-era shape, and the registry may have no copy of what it
78
+ * points at; a real directory is a registry install the remote can fetch itself.
79
+ * @param {string} name - Package name.
80
+ * @param {string} fromDir - Directory whose manifest declares it.
81
+ * @returns {string|null} Absolute real path of the linked checkout, or null.
82
+ */
83
+ function resolveLinkedPackage(name, fromDir) {
84
+ let current = path.resolve(fromDir);
85
+
86
+ while (true) {
87
+ const candidate = path.join(current, 'node_modules', name);
88
+ const entry = jetpack.inspect(candidate);
89
+
90
+ if (entry) {
91
+ if (entry.type !== 'symlink') {
92
+ return null;
93
+ }
94
+ const real = fs.realpathSync(candidate);
95
+ return jetpack.exists(path.join(real, 'package.json')) ? real : null;
96
+ }
97
+
98
+ const parent = path.dirname(current);
99
+ if (parent === current) {
100
+ return null;
101
+ }
102
+ current = parent;
103
+ }
104
+ }
105
+
106
+ /** The workspace globs a manifest declares, in either supported spelling. */
107
+ function workspaceGlobs(pkg) {
108
+ const workspaces = (pkg || {}).workspaces;
109
+ if (!workspaces) {
110
+ return [];
111
+ }
112
+
113
+ return Array.isArray(workspaces) ? workspaces : (workspaces.packages || []);
114
+ }
115
+
116
+ /**
117
+ * Every manifest an install of `dir` resolves: the root's, then each workspace
118
+ * member's.
119
+ * @param {string} dir - The install root.
120
+ * @param {object} pkg - Its parsed package.json.
121
+ * @returns {Array<{ dir: string, file: string, pkg: object, root: boolean }>} The manifests.
122
+ */
123
+ function collectManifests(dir, pkg) {
124
+ const manifests = [{ dir, file: path.join(dir, 'package.json'), pkg, root: true }];
125
+
126
+ for (const pattern of workspaceGlobs(pkg)) {
127
+ for (const match of jetpack.find(dir, { matching: `${pattern}/package.json`, files: true }) || []) {
128
+ const file = path.resolve(match);
129
+ // An install never reads a manifest out of node_modules as a member
130
+ if (file.split(path.sep).includes('node_modules')) {
131
+ continue;
132
+ }
133
+
134
+ const member = jetpack.read(file, 'json');
135
+ if (member) {
136
+ manifests.push({ dir: path.dirname(file), file, pkg: member, root: false });
137
+ }
138
+ }
139
+ }
140
+
141
+ return manifests;
142
+ }
143
+
144
+ /**
145
+ * The nearest ancestor manifest declaring workspaces: the install root `dir`
146
+ * belonged to before it became one of its own, and therefore the home of the
147
+ * overrides it inherits.
148
+ * @param {string} dir - The install root being staged.
149
+ * @returns {{ dir: string, pkg: object }|null} The enclosing workspace root.
150
+ */
151
+ function enclosingWorkspaceRoot(dir) {
152
+ let current = path.dirname(path.resolve(dir));
153
+
154
+ while (true) {
155
+ const pkg = jetpack.read(path.join(current, 'package.json'), 'json');
156
+ if (pkg && workspaceGlobs(pkg).length) {
157
+ return { dir: current, pkg };
158
+ }
159
+
160
+ const parent = path.dirname(current);
161
+ if (parent === current) {
162
+ return null;
163
+ }
164
+ current = parent;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Every local package the install must carry, and where each one is named.
170
+ *
171
+ * Two shapes ride together: an ENTRY is one place a manifest names a package
172
+ * (respelled in place, or redirected by an override on the root), and a SOURCE
173
+ * is the package itself (packed or copied exactly once, however many entries
174
+ * name it).
175
+ *
176
+ * @param {object} options - Options.
177
+ * @param {string} options.dir - The install root being staged.
178
+ * @param {Array<object>} options.manifests - collectManifests() output.
179
+ * @returns {{ entries: Array<object>, sources: Array<{ name: string, from: string }> }}
180
+ * Sources in pack order: direct first, then the linked ones they pull in.
181
+ */
182
+ function collectTargets({ dir, manifests }) {
183
+ const entries = [];
184
+ const sources = [];
185
+ const claimed = new Set();
186
+ const rootManifest = manifests[0];
187
+
188
+ const add = (entry) => {
189
+ entries.push(entry);
190
+ if (!claimed.has(entry.name)) {
191
+ claimed.add(entry.name);
192
+ sources.push({ name: entry.name, from: entry.from });
193
+ }
194
+ };
195
+
196
+ // Direct: file: specs resolving OUTSIDE the dir, per manifest. A spec already
197
+ // pointing inside is what the install carries, so it stays as it is.
198
+ for (const manifest of manifests) {
199
+ for (const block of DEP_BLOCKS) {
200
+ for (const [name, spec] of Object.entries(manifest.pkg[block] || {})) {
201
+ if (!spec.startsWith(FILE_SPEC)) {
202
+ continue;
203
+ }
204
+
205
+ const from = path.resolve(manifest.dir, spec.slice(FILE_SPEC.length));
206
+ if (!isInside(dir, from)) {
207
+ add({ name, from, manifest, block });
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ // Inherited: the overrides of the workspace root this dir is a member of, the
214
+ // ones npm would have applied to it there (the second hop's linked client).
215
+ const enclosing = enclosingWorkspaceRoot(dir);
216
+ const inherited = enclosing ? enclosing.pkg.overrides || {} : {};
217
+ const ownOverrides = rootManifest.pkg.overrides || {};
218
+
219
+ for (const [name, spec] of Object.entries({ ...inherited, ...ownOverrides })) {
220
+ if (typeof spec !== 'string' || !spec.startsWith(FILE_SPEC) || claimed.has(name)) {
221
+ continue;
222
+ }
223
+
224
+ const base = ownOverrides[name] === spec ? rootManifest.dir : enclosing.dir;
225
+ const from = path.resolve(base, spec.slice(FILE_SPEC.length));
226
+ if (!isInside(dir, from)) {
227
+ add({ name, from, manifest: rootManifest, block: null });
228
+ }
229
+ }
230
+
231
+ // Transitive: each packed package's linked @omega.js runtime deps. A copied
232
+ // tarball has none to walk: whatever it needed was resolved a hop ago and
233
+ // rides along as an inherited override.
234
+ for (let index = 0; index < sources.length; index++) {
235
+ const source = sources[index];
236
+ if (source.from.endsWith('.tgz')) {
237
+ continue;
238
+ }
239
+
240
+ const manifest = jetpack.read(path.join(source.from, 'package.json'), 'json');
241
+ for (const name of Object.keys((manifest || {}).dependencies || {})) {
242
+ if (!name.startsWith(SCOPE) || claimed.has(name)) {
243
+ continue;
244
+ }
245
+
246
+ const from = resolveLinkedPackage(name, source.from);
247
+ if (!from) {
248
+ continue; // Registry-installed (or absent): npm resolves it the normal way
249
+ }
250
+
251
+ add({ name, from, manifest: rootManifest, block: null });
252
+ }
253
+ }
254
+
255
+ return { entries, sources };
256
+ }
257
+
258
+ /**
259
+ * Stage an install root's local packages into it.
260
+ *
261
+ * @param {object} options - Options.
262
+ * @param {string} options.dir - The install root: a brand root, a target, or a
263
+ * generated functions folder.
264
+ * @param {Function} [options.log] - One line per step (silent by default).
265
+ * @returns {Promise<{ staged: string[], restore: Function }>} The packed names,
266
+ * and the restore that puts the tree back exactly as it was found.
267
+ */
268
+ async function stageLocalPackages({ dir, log = () => {} }) {
269
+ const root = path.resolve(dir);
270
+ const lockPath = path.join(root, 'package-lock.json');
271
+ const stagingPath = path.join(root, STAGING_DIR);
272
+
273
+ const pkg = jetpack.read(path.join(root, 'package.json'), 'json');
274
+ if (!pkg) {
275
+ return { staged: [], restore: async () => {} };
276
+ }
277
+
278
+ const manifests = collectManifests(root, pkg);
279
+ const rootManifest = manifests[0];
280
+ const { entries, sources } = collectTargets({ dir: root, manifests });
281
+
282
+ if (!sources.length) {
283
+ return { staged: [], restore: async () => {} };
284
+ }
285
+
286
+ // Originals, restored verbatim after the deploy and after a failure
287
+ const touched = [...new Set(entries.map((entry) => entry.manifest))];
288
+ const originals = touched.map((manifest) => ({ file: manifest.file, contents: jetpack.read(manifest.file) }));
289
+ const originalLock = jetpack.read(lockPath);
290
+
291
+ const restore = async () => {
292
+ for (const original of originals) {
293
+ jetpack.write(original.file, original.contents);
294
+ }
295
+
296
+ if (originalLock === undefined) {
297
+ jetpack.remove(lockPath);
298
+ } else {
299
+ jetpack.write(lockPath, originalLock);
300
+ }
301
+
302
+ jetpack.remove(stagingPath);
303
+ };
304
+
305
+ try {
306
+ jetpack.remove(stagingPath);
307
+ jetpack.dir(stagingPath);
308
+
309
+ const tarballs = {};
310
+
311
+ for (const source of sources) {
312
+ // A `.tgz` was packed a hop ago, by the stage that produced the tree this
313
+ // one is staging out of: copying it keeps the artifact identical and asks
314
+ // nothing of sources that are a machine away.
315
+ if (source.from.endsWith('.tgz')) {
316
+ if (!jetpack.exists(source.from)) {
317
+ throw new Error(`Local dependency ${source.name} names a tarball that is not there: ${source.from}`);
318
+ }
319
+
320
+ const filename = path.basename(source.from);
321
+ jetpack.copy(source.from, path.join(stagingPath, filename));
322
+ tarballs[source.name] = filename;
323
+ log(` ${chalk.green('✓')} Copied ${chalk.cyan(source.name)} → ${STAGING_DIR}/${filename}`);
324
+ continue;
325
+ }
326
+
327
+ if (!jetpack.exists(path.join(source.from, 'package.json'))) {
328
+ throw new Error(`Local dependency ${source.name} has no package.json at ${source.from}`);
329
+ }
330
+
331
+ log(` ${chalk.dim('→')} Packing local package ${chalk.cyan(source.name)}...`);
332
+
333
+ const output = await powertools.execute(
334
+ `npm pack --pack-destination "${stagingPath}" --foreground-scripts=false`,
335
+ { log: false, config: { cwd: source.from } },
336
+ );
337
+
338
+ const filename = String(output).trim().split('\n').pop().trim();
339
+ if (!filename.endsWith('.tgz') || !jetpack.exists(path.join(stagingPath, filename))) {
340
+ throw new Error(`npm pack for ${source.name} did not produce a tarball (got: ${filename || 'nothing'})`);
341
+ }
342
+
343
+ tarballs[source.name] = filename;
344
+ log(` ${chalk.green('✓')} Staged ${chalk.cyan(source.name)} → ${STAGING_DIR}/${filename}`);
345
+ }
346
+
347
+ const overrides = {};
348
+
349
+ for (const entry of entries) {
350
+ const tarball = path.join(stagingPath, tarballs[entry.name]);
351
+
352
+ if (entry.block) {
353
+ // Relative to the manifest that names it: a member's spec climbs out of
354
+ // its own folder, the root's does not.
355
+ entry.manifest.pkg[entry.block][entry.name] = fileSpec(entry.manifest.dir, tarball);
356
+ } else {
357
+ // A packed package still declares its own REGISTRY spec internally
358
+ // (`@omega.js/client: ^0.1.0`), so only an override redirects that
359
+ // nested resolution to the artifact beside it.
360
+ overrides[entry.name] = fileSpec(root, tarball);
361
+ }
362
+ }
363
+
364
+ if (Object.keys(overrides).length > 0) {
365
+ rootManifest.pkg.overrides = Object.assign({}, rootManifest.pkg.overrides, overrides);
366
+ }
367
+
368
+ for (const manifest of touched) {
369
+ jetpack.write(manifest.file, `${JSON.stringify(manifest.pkg, null, 2)}\n`);
370
+ }
371
+
372
+ // A remote install runs `npm ci`, so the uploaded lockfile must match the
373
+ // staged shape, regenerated from scratch (the one helper keeps it THIS
374
+ // dir's lock, never an enclosing workspace root's).
375
+ log(` ${chalk.dim('→')} Regenerating package-lock.json for the staged shape...`);
376
+ jetpack.remove(lockPath);
377
+ await regenerateLockfile({ root });
378
+ } catch (error) {
379
+ // Loud and clean: the deploy stops here, and the tree goes back exactly as
380
+ // it was rather than sitting half-staged.
381
+ await restore();
382
+ throw new Error(`Local-package staging failed, nothing deployed and ${path.basename(root)}/ restored:\n${error.message}`);
383
+ }
384
+
385
+ return { staged: sources.map((source) => source.name), restore };
386
+ }
387
+
388
+ 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,142 @@
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: the WHOLE slug GitHub answers against
36
+ * the source repo the config derives (`@omega.js/config`'s `repoDrift`, so a
37
+ * rename drifts as surely as a transfer,
38
+ * [#934](https://github.com/Omega-JS-Stack/omega/issues/934)), stated in one
39
+ * line and nothing more. Never fatal: this runs before every verb, and a fatal
40
+ * boot would lock out the verbs that fix the mismatch. The verbs that ACT on
41
+ * the derived repo (the manage walk, the deploy) refuse on the same line
42
+ * instead. The prelude never writes config: which of the two is wrong (the
43
+ * config, or where the repo lives) is a human's call.
44
+ *
45
+ * The WRITE is the one thing that fails loudly: a `git remote set-url` refused
46
+ * after a `get-url` just answered is not an external condition, it is a broken
47
+ * invariant, and the runner stops the boot on it.
48
+ */
49
+
50
+ const { readOrigin, retargetRemoteUrl, setRemoteUrl } = require('../git-remote.js');
51
+ const github = require('../github-repo.js');
52
+
53
+ // The one network read runs on EVERY verb boot, so a network that hangs (a
54
+ // captive portal, a dropped link) is bounded here and lands on the same silent
55
+ // `unreachable` branch an offline machine does.
56
+ const GITHUB_READ_TIMEOUT_MS = 10000;
57
+
58
+ /**
59
+ * @param {object} context - The prelude context.
60
+ * @param {string|null} context.brandRoot - The brand root of the invocation.
61
+ * @param {object} [context.config] - The composed omega config, when the caller has it.
62
+ * @param {function} [context.execFn] - Injectable git exec (tests).
63
+ * @param {function} [context.resolveRepo] - Injectable `(owner, name) => repo|null` (tests).
64
+ * @param {function} [context.log] - Injectable line printer (tests).
65
+ * @returns {{ healed: boolean, reason?: string, from?: string, to?: string, drift?: { origin: string, derived: string } }}
66
+ */
67
+ function run(context = {}) {
68
+ const { brandRoot, execFn } = context;
69
+ const log = context.log || console.log;
70
+ // Called through the module so the ONE network read has a seam a real CLI
71
+ // boot can stub too (the integration test patches it here).
72
+ const resolveRepo = context.resolveRepo || ((owner, name) => github.getRepo(owner, name, { timeout: GITHUB_READ_TIMEOUT_MS }));
73
+
74
+ if (!brandRoot) return { healed: false, reason: 'no-brand' };
75
+
76
+ // One stat before anything else (most boots in this monorepo's own trees and
77
+ // in every fixture brand end there), then the remote itself.
78
+ const current = readOrigin({ dir: brandRoot, execFn });
79
+ if (!current.slug) return { healed: false, reason: current.reason };
80
+
81
+ const { url, slug: from } = current;
82
+
83
+ // The ONE network read, on the slug the checkout carries: GitHub follows its
84
+ // own redirect and names where that repo lives now.
85
+ let resolved;
86
+ try {
87
+ resolved = resolveRepo(current.owner, current.repo);
88
+ } catch (e) {
89
+ return { healed: false, reason: 'unreachable' };
90
+ }
91
+ if (!resolved) return { healed: false, reason: 'missing' };
92
+
93
+ const to = resolved.full_name;
94
+ let result;
95
+
96
+ if (to.toLowerCase() === from.toLowerCase()) {
97
+ result = { healed: false, reason: 'converged' };
98
+ } else {
99
+ setRemoteUrl({ dir: brandRoot, url: retargetRemoteUrl(url, to), execFn });
100
+ log(`omega: origin healed from ${from} to ${to}`);
101
+ result = { healed: true, from, to };
102
+ }
103
+
104
+ const config = composedConfig(brandRoot, context.config);
105
+ const { repoDrift, sourceRepo } = require('../../config/index.js');
106
+ const drift = config ? repoDrift(to, config) : null;
107
+
108
+ if (drift) {
109
+ log(`omega: ${drift}`);
110
+ result.drift = { origin: to, derived: sourceRepo(config).slug };
111
+ }
112
+
113
+ return result;
114
+ }
115
+
116
+ /**
117
+ * The composed config the drift is read against: the caller's when it has one,
118
+ * else loaded here, and an unloadable one answers null ("no drift"): a config
119
+ * the boot cannot read is the VERB's failure to report, in its own words, never
120
+ * a prelude's.
121
+ *
122
+ * @param {string} brandRoot - The brand root.
123
+ * @param {object} [config] - The composed config, when the caller has it.
124
+ * @returns {object|null}
125
+ */
126
+ function composedConfig(brandRoot, config) {
127
+ if (config) return config;
128
+
129
+ const { loadConfig } = require('../../config/index.js');
130
+
131
+ try {
132
+ return loadConfig(brandRoot).config;
133
+ } catch (e) {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ module.exports = {
139
+ name: 'origin-heal',
140
+ verbs: 'all',
141
+ run,
142
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The Firebase service-account key's ONE lookup
3
+ * ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)), and the ONE
4
+ * spelling of every path under the brand's secrets folder
5
+ * ([#897](https://github.com/Omega-JS-Stack/omega/issues/897)).
6
+ *
7
+ * The key is a FILE the cloud manage service mints, never a `.env` value, so
8
+ * everything that needs it (the backend stage, the deploy-roles read, the
9
+ * secrets publish that sends it to a runner) resolves the same two candidates
10
+ * in the same order: the target root's own, then the brand's secrets folder.
11
+ * It lived in the backend's stage helper while the backend was its only reader;
12
+ * the shared secrets publisher reads it now, and devkit never requires a
13
+ * framework, so the lookup lives here and the backend imports it.
14
+ *
15
+ * The Google OAuth client the authentication service writes obeys the same
16
+ * folder rule, so it is named here too: a rename of the folder is one edit,
17
+ * not a hunt through every manager and backend reader.
18
+ */
19
+ const path = require('node:path');
20
+ const jetpack = require('fs-jetpack');
21
+ const { findBrandRoot } = require('../config/index.js');
22
+
23
+ // The brand-level home of every minted credential, and the two files in it.
24
+ const SECRETS_DIR = path.join('.omega', 'secrets');
25
+ const SERVICE_ACCOUNT_REL = path.join(SECRETS_DIR, 'service-account.json');
26
+ const GOOGLE_OAUTH_REL = path.join(SECRETS_DIR, 'google-oauth.json');
27
+
28
+ /**
29
+ * The service-account key a target deploys with, or null when none is minted.
30
+ *
31
+ * @param {string} projectDir - The target root.
32
+ * @returns {string|null} Absolute path to the key file.
33
+ */
34
+ function resolveServiceAccountPath(projectDir) {
35
+ const brandRoot = findBrandRoot(projectDir);
36
+
37
+ return [
38
+ path.join(projectDir, 'service-account.json'),
39
+ brandRoot ? path.join(brandRoot, SERVICE_ACCOUNT_REL) : null,
40
+ ].filter(Boolean).find((candidate) => jetpack.exists(candidate)) || null;
41
+ }
42
+
43
+ module.exports = { resolveServiceAccountPath, SECRETS_DIR, SERVICE_ACCOUNT_REL, GOOGLE_OAUTH_REL };