@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,73 @@
1
+ /**
2
+ * git-auth: how a GitHub token reaches `git` for ONE push, and how it is kept
3
+ * out of everything that prints ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)).
4
+ *
5
+ * Two lanes push to a repo the checkout is not authenticated for: the snapshot
6
+ * push of `deploy-snapshot.js` and the web deploy's gh-pages push. Both need
7
+ * the same three facts, and both used to spell them themselves, which is how a
8
+ * credential ends up in an argv in one of them and not the other:
9
+ *
10
+ * - the credential NEVER enters argv or a url. A push url carrying a token is
11
+ * readable by every other process on the machine, is printed by git's own
12
+ * error output, and rides the message `execFileSync` throws (which carries
13
+ * the whole argv) straight into a CI log, outside Actions' masking. It
14
+ * travels in the config ENV of that one child process instead, which lives
15
+ * exactly as long as the push and lands on no disk;
16
+ * - the header is the BASIC form (`x-access-token:<token>`, base64), the one
17
+ * the checkout action uses: the git endpoints are not the REST API and
18
+ * ignore a bearer header, so the push falls back to a username prompt
19
+ * (proven on the first playground push, [#872](https://github.com/Omega-JS-Stack/omega/issues/872));
20
+ * - a failure is rethrown SCRUBBED, in both the raw form and the base64 the
21
+ * header carries, so no lane can print what it was careful not to pass.
22
+ */
23
+
24
+ // The one config entry either lane sets: git applies `http.<url>.extraheader`
25
+ // to every request under that url prefix.
26
+ const EXTRAHEADER_KEY = 'http.https://github.com/.extraheader';
27
+
28
+ /**
29
+ * The token in the form the header carries it.
30
+ *
31
+ * @param {string} token - GitHub token.
32
+ * @returns {string} base64 of `x-access-token:<token>`.
33
+ */
34
+ function gitAuthValue(token) {
35
+ return Buffer.from(`x-access-token:${token}`).toString('base64');
36
+ }
37
+
38
+ /**
39
+ * The environment entries that hand `git` the token through its own config, for
40
+ * the lifetime of one child process.
41
+ *
42
+ * @param {string} [token] - GitHub token; without one the result is empty, so a
43
+ * spread of it is a no-op and git asks for credentials itself.
44
+ * @returns {object} `GIT_CONFIG_COUNT`/`GIT_CONFIG_KEY_0`/`GIT_CONFIG_VALUE_0`, or `{}`.
45
+ */
46
+ function gitAuthEnv(token) {
47
+ if (!token) return {};
48
+
49
+ return {
50
+ GIT_CONFIG_COUNT: '1',
51
+ GIT_CONFIG_KEY_0: EXTRAHEADER_KEY,
52
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${gitAuthValue(token)}`,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Every appearance of the token replaced by a placeholder, raw and base64, so a
58
+ * failure can be printed.
59
+ *
60
+ * @param {string} message - Text to scrub.
61
+ * @param {string} [token] - The token to remove (absent scrubs nothing).
62
+ * @returns {string} The text with the token redacted.
63
+ */
64
+ function scrubToken(message, token) {
65
+ const text = String(message === undefined || message === null ? '' : message);
66
+ if (!token) return text;
67
+
68
+ return text
69
+ .split(token).join('***')
70
+ .split(gitAuthValue(token)).join('***');
71
+ }
72
+
73
+ module.exports = { gitAuthEnv, gitAuthValue, scrubToken, EXTRAHEADER_KEY };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The git REMOTE boundary: the url a checkout points at, parsed, retargeted and
3
+ * written ([#890](https://github.com/Omega-JS-Stack/omega/issues/890)).
4
+ *
5
+ * `parseRemoteUrl` lived in `deploy.js`, whose load pulls in the local-link,
6
+ * pack and snapshot modules: 40ms nothing on the CLI BOOT path can spend for
7
+ * one regex. The boot prelude that heals a redirected `origin` needs the parse
8
+ * plus the two git calls around it, so the three live here, in one place, and
9
+ * `deploy.js` re-exports the parse it already published.
10
+ *
11
+ * Transport: `git` through execFile with an ARGV ARRAY, never a shell string,
12
+ * so a remote url can never inject a command. Every call names its directory
13
+ * with `-C`: git resolves a repo by walking UP, and a brand nested inside
14
+ * another repo would otherwise read (and rewrite) the enclosing repo's
15
+ * remote.
16
+ */
17
+
18
+ const { execFileSync } = require('node:child_process');
19
+
20
+ /**
21
+ * Parse a git remote URL into { owner, repo }.
22
+ * Handles ssh (git@github.com:o/r.git), https (https://github.com/o/r.git),
23
+ * and .git-less forms.
24
+ * @param {string} url - remote URL
25
+ * @returns {{ owner: string, repo: string }|null} Null for anything that is not a GitHub remote.
26
+ */
27
+ function parseRemoteUrl(url) {
28
+ const match = (url || '').trim().match(/github\.com[/:]([^/]+)\/([^/\s]+?)(?:\.git)?$/);
29
+ if (!match) return null;
30
+ return { owner: match[1], repo: match[2] };
31
+ }
32
+
33
+ /**
34
+ * The same remote url, addressing another repo: the slug is substituted in
35
+ * place, so the url keeps its OWN form (an ssh remote stays ssh, an https
36
+ * remote stays https, a `.git` suffix stays or stays absent). A rewrite that
37
+ * normalized the form would change how the checkout authenticates.
38
+ *
39
+ * @param {string} url - The current remote url.
40
+ * @param {string} slug - The `owner/name` to point it at.
41
+ * @returns {string} The retargeted url.
42
+ * @throws {Error} When the url is not a GitHub remote (the caller parsed it already).
43
+ */
44
+ function retargetRemoteUrl(url, slug) {
45
+ const current = parseRemoteUrl(url);
46
+ if (!current) {
47
+ throw new Error(`Cannot retarget a remote url that names no GitHub repo: ${String(url).trim() || '(none)'}`);
48
+ }
49
+
50
+ const trimmed = url.trim();
51
+ const currentSlug = `${current.owner}/${current.repo}`;
52
+ const at = trimmed.lastIndexOf(currentSlug);
53
+
54
+ return trimmed.slice(0, at) + slug + trimmed.slice(at + currentSlug.length);
55
+ }
56
+
57
+ /**
58
+ * A remote's url, as the checkout has it.
59
+ * @param {object} options
60
+ * @param {string} options.dir - The repository directory.
61
+ * @param {string} [options.remote] - Remote name (default: origin).
62
+ * @param {function} [options.execFn] - Injectable `(file, args, opts) => stdout` (tests).
63
+ * @returns {string} The trimmed url.
64
+ * @throws {Error} When git has no such remote, or the directory is no repository.
65
+ */
66
+ function remoteUrl(options) {
67
+ const execFn = options.execFn || execFileSync;
68
+ const args = ['-C', options.dir, 'remote', 'get-url', options.remote || 'origin'];
69
+
70
+ return String(execFn('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })).trim();
71
+ }
72
+
73
+ /**
74
+ * Point a remote at another url.
75
+ * @param {object} options
76
+ * @param {string} options.dir - The repository directory.
77
+ * @param {string} options.url - The url to set.
78
+ * @param {string} [options.remote] - Remote name (default: origin).
79
+ * @param {function} [options.execFn] - Injectable exec (tests).
80
+ * @returns {void}
81
+ * @throws {Error} When git refuses the write.
82
+ */
83
+ function setRemoteUrl(options) {
84
+ const execFn = options.execFn || execFileSync;
85
+ const args = ['-C', options.dir, 'remote', 'set-url', options.remote || 'origin', options.url];
86
+
87
+ execFn('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
88
+ }
89
+
90
+ module.exports = {
91
+ parseRemoteUrl,
92
+ retargetRemoteUrl,
93
+ remoteUrl,
94
+ setRemoteUrl,
95
+ };
@@ -0,0 +1,290 @@
1
+ /**
2
+ * The GitHub repo boundary, ONE home
3
+ * ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)).
4
+ *
5
+ * Three lanes used to talk to GitHub their own way: the manager's repo service
6
+ * (a gh-CLI wrapper), @omega.js/desktop's release precheck (an Octokit client
7
+ * with its own `ensureRepo`), and the extension's publish workflow (a `gh
8
+ * release` shell step inside a YAML template). Same four questions, three
9
+ * answers, three places to fix a bug. This module is the answer: every repo a
10
+ * brand owns is created, reconciled and served from here.
11
+ *
12
+ * Transport: the `gh` CLI through execFile with an ARGV ARRAY, never a shell
13
+ * string, so a brand-config value (a description, a homepage, a domain) can
14
+ * never inject a command. `gh` is already required by the manager and ships on
15
+ * every GitHub runner. Auth is the ambient `gh auth` session or a
16
+ * GH_TOKEN/GITHUB_TOKEN in the env, which gh honors on its own: nothing here
17
+ * reads, logs or passes a token.
18
+ *
19
+ * Every ensure is IDEMPOTENT and returns the same shape,
20
+ * `{ created, changed, planned }`, where `planned` is the human-readable list
21
+ * of what the call does (or WOULD do under `dryRun`), so a dry run prints the
22
+ * plan and a real run reports the same lines.
23
+ */
24
+
25
+ const { execFileSync } = require('node:child_process');
26
+
27
+ // GitHub Pages serves the built site from one orphan branch, the same one
28
+ // every OMEGA web deploy force-pushes.
29
+ const DEFAULT_PAGES_BRANCH = 'gh-pages';
30
+
31
+ // The plan name GitHub reports for a free org, and the answer for an owner
32
+ // with no plan to read (a user account): the two cases that cannot host a
33
+ // private Pages site.
34
+ const FREE_PLAN = 'free';
35
+
36
+ /**
37
+ * Run `gh` with an argv array and return its trimmed stdout.
38
+ *
39
+ * @param {string[]} args - The argv after `gh` (e.g. ['api', 'repos/o/r']).
40
+ * @param {object} [options]
41
+ * @param {function} [options.execFn] - Injectable `(file, args, opts) => stdout` (tests).
42
+ * @param {number} [options.timeout] - Milliseconds before the call is given up on (default: none).
43
+ * @returns {string} Trimmed stdout ('' when gh printed nothing).
44
+ * @throws {Error} When gh exits non-zero or times out, carrying its stderr.
45
+ */
46
+ function gh(args, options = {}) {
47
+ const execFn = options.execFn || execFileSync;
48
+
49
+ try {
50
+ return String(execFn('gh', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: options.timeout })).trim();
51
+ } catch (error) {
52
+ const stderr = (error.stderr && error.stderr.toString()) || error.message;
53
+ throw new Error(`gh ${args.join(' ')} failed: ${stderr.trim()}`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Run a `gh` call whose output is JSON.
59
+ * @param {string[]} args - The argv after `gh`.
60
+ * @param {object} [options] - See gh().
61
+ * @returns {object|null} The parsed body, or null when gh printed nothing.
62
+ */
63
+ function ghJson(args, options = {}) {
64
+ const out = gh(args, options);
65
+ return out ? JSON.parse(out) : null;
66
+ }
67
+
68
+ /**
69
+ * A missing resource, told from a real failure: gh prints the status line, so a
70
+ * 404 is the answer "it does not exist" and anything else is a problem to raise.
71
+ * @param {Error} error - The error gh() threw.
72
+ * @returns {boolean}
73
+ */
74
+ function isNotFound(error) {
75
+ return /404|Not Found/i.test(error.message);
76
+ }
77
+
78
+ /**
79
+ * A repository's API record.
80
+ * @param {string} owner - Org or user.
81
+ * @param {string} name - Repo name.
82
+ * @param {object} [options] - See gh().
83
+ * @returns {object|null} The repo json, or null when it does not exist.
84
+ */
85
+ function getRepo(owner, name, options = {}) {
86
+ try {
87
+ return ghJson(['api', `repos/${owner}/${name}`], options);
88
+ } catch (error) {
89
+ if (isNotFound(error)) return null;
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Create a repo when it is missing, else reconcile the two facts a brand
96
+ * declares about it: its visibility and its homepage. The description is a
97
+ * create-time value (nothing derives it after the fact).
98
+ *
99
+ * @param {object} repo - The repo to ensure.
100
+ * @param {string} repo.owner - Org or user.
101
+ * @param {string} repo.name - Repo name.
102
+ * @param {boolean} [repo.private] - Visibility (default private).
103
+ * @param {string} [repo.description] - Set when the repo is created.
104
+ * @param {string} [repo.homepage] - The repo's url field, reconciled on drift.
105
+ * @param {string} [repo.reason] - Why this visibility, stated on the create line
106
+ * the plan prints. The CALLER owns the why (a website repo is public because
107
+ * the org is on the free plan, which only the manage walk knows), so it hands
108
+ * the words in rather than reformatting a line it matched by string.
109
+ * @param {boolean} [repo.autoInit] - Create with a first commit. A repo that
110
+ * must carry a commit before anything can publish to it (the releases repo:
111
+ * a release needs a tag, and a tag needs a commit) asks for it; a repo whose
112
+ * history is pushed from a machine never does, since an initial commit it did
113
+ * not make is a push it has to fight.
114
+ * @param {object} [options]
115
+ * @param {boolean} [options.dryRun] - Build the plan and touch nothing.
116
+ * @param {function} [options.execFn] - Injectable exec (tests).
117
+ * @returns {{ created: boolean, changed: boolean, planned: string[] }}
118
+ */
119
+ function ensureRepo(repo, options = {}) {
120
+ const { owner, name } = repo;
121
+ const slug = `${owner}/${name}`;
122
+ const isPrivate = repo.private !== false;
123
+ const existing = getRepo(owner, name, options);
124
+
125
+ if (!existing) {
126
+ const planned = [`create ${slug} (${isPrivate ? 'private' : 'public'}${repo.reason ? `: ${repo.reason}` : ''})`];
127
+ if (options.dryRun) return { created: false, changed: false, planned };
128
+
129
+ const args = ['repo', 'create', slug, isPrivate ? '--private' : '--public'];
130
+ if (repo.description) args.push('--description', repo.description);
131
+ if (repo.homepage) args.push('--homepage', repo.homepage);
132
+ if (repo.autoInit) args.push('--add-readme');
133
+ gh(args, options);
134
+
135
+ return { created: true, changed: false, planned };
136
+ }
137
+
138
+ const fields = [];
139
+ const planned = [];
140
+
141
+ if (existing.private !== isPrivate) {
142
+ fields.push('-F', `private=${isPrivate}`);
143
+ planned.push(`${slug}: ${isPrivate ? 'public -> private' : 'private -> public'}`);
144
+ }
145
+
146
+ if (repo.homepage && existing.homepage !== repo.homepage) {
147
+ fields.push('-f', `homepage=${repo.homepage}`);
148
+ planned.push(`${slug}: homepage -> ${repo.homepage}`);
149
+ }
150
+
151
+ if (!fields.length) return { created: false, changed: false, planned };
152
+ if (options.dryRun) return { created: false, changed: false, planned };
153
+
154
+ gh(['api', `repos/${slug}`, '-X', 'PATCH', ...fields], options);
155
+
156
+ return { created: false, changed: true, planned };
157
+ }
158
+
159
+ /**
160
+ * A repo's GitHub Pages configuration.
161
+ * @param {string} owner - Org or user.
162
+ * @param {string} name - Repo name.
163
+ * @param {object} [options] - See gh().
164
+ * @returns {object|null} The pages json, or null when Pages is off.
165
+ */
166
+ function getPages(owner, name, options = {}) {
167
+ try {
168
+ return ghJson(['api', `repos/${owner}/${name}/pages`], options);
169
+ } catch (error) {
170
+ if (isNotFound(error)) return null;
171
+ throw error;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Whether a branch exists on a repo.
177
+ * @param {string} owner - Org or user.
178
+ * @param {string} name - Repo name.
179
+ * @param {string} branch - Branch name.
180
+ * @param {object} [options] - See gh().
181
+ * @returns {boolean}
182
+ */
183
+ function branchExists(owner, name, branch, options = {}) {
184
+ try {
185
+ ghJson(['api', `repos/${owner}/${name}/branches/${branch}`], options);
186
+ return true;
187
+ } catch (error) {
188
+ if (isNotFound(error)) return false;
189
+ throw error;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Point GitHub Pages at the deploy branch and, when the target has one, at its
195
+ * custom domain. Idempotent: a repo already serving that branch at that domain
196
+ * is left alone.
197
+ *
198
+ * Pages cannot be configured before the branch exists, and the branch is what
199
+ * the FIRST deploy pushes, so a fresh website repo reports `pending` instead of
200
+ * failing: the next walk after the first deploy finishes the job.
201
+ *
202
+ * @param {object} pages - What to serve.
203
+ * @param {string} pages.owner - Org or user.
204
+ * @param {string} pages.name - Repo name.
205
+ * @param {string} [pages.branch] - Source branch (default gh-pages).
206
+ * @param {string} [pages.cname] - Custom domain (the host the target is served at).
207
+ * @param {object} [options]
208
+ * @param {boolean} [options.dryRun] - Build the plan and touch nothing.
209
+ * @param {function} [options.execFn] - Injectable exec (tests).
210
+ * @param {object} [options.logger] - `{ log }` (default: console).
211
+ * @returns {{ created: boolean, changed: boolean, planned: string[], pending?: boolean }}
212
+ */
213
+ function ensurePages(pages, options = {}) {
214
+ const { owner, name, cname } = pages;
215
+ const branch = pages.branch || DEFAULT_PAGES_BRANCH;
216
+ const slug = `${owner}/${name}`;
217
+ const logger = options.logger || console;
218
+
219
+ if (!branchExists(owner, name, branch, options)) {
220
+ logger.log(`${slug} has no ${branch} branch yet: Pages configures after the first deploy`);
221
+ return { created: false, changed: false, planned: [], pending: true };
222
+ }
223
+
224
+ const existing = getPages(owner, name, options);
225
+ const planned = [];
226
+
227
+ if (!existing) {
228
+ planned.push(`pages ${slug}: ${branch}${cname ? ` -> ${cname}` : ''}`);
229
+ if (options.dryRun) return { created: false, changed: false, planned };
230
+
231
+ gh(['api', `repos/${slug}/pages`, '-X', 'POST', '-f', `source[branch]=${branch}`, '-f', 'source[path]=/'], options);
232
+ if (cname) gh(['api', `repos/${slug}/pages`, '-X', 'PUT', '-f', `cname=${cname}`], options);
233
+
234
+ return { created: true, changed: false, planned };
235
+ }
236
+
237
+ const sourceBranch = existing.source ? existing.source.branch : undefined;
238
+ let changed = false;
239
+
240
+ if (sourceBranch !== branch) {
241
+ planned.push(`pages ${slug}: ${sourceBranch || 'none'} -> ${branch}`);
242
+ if (!options.dryRun) {
243
+ gh(['api', `repos/${slug}/pages`, '-X', 'PUT', '-f', `source[branch]=${branch}`, '-f', 'source[path]=/'], options);
244
+ changed = true;
245
+ }
246
+ }
247
+
248
+ if (cname && existing.cname !== cname) {
249
+ planned.push(`pages ${slug}: domain -> ${cname}`);
250
+ if (!options.dryRun) {
251
+ gh(['api', `repos/${slug}/pages`, '-X', 'PUT', '-f', `cname=${cname}`], options);
252
+ changed = true;
253
+ }
254
+ }
255
+
256
+ return { created: false, changed, planned };
257
+ }
258
+
259
+ /**
260
+ * An owner's GitHub plan, which is what says whether a private repo may serve
261
+ * Pages at all: only a paid org can. A USER owner and an org whose plan the
262
+ * token cannot read both answer `free`, the half that cannot, because guessing
263
+ * the other way publishes a site the brand meant to keep private.
264
+ *
265
+ * @param {string} owner - Org or user.
266
+ * @param {object} [options] - See gh().
267
+ * @returns {string} 'free' or the plan name.
268
+ */
269
+ function ownerPlan(owner, options = {}) {
270
+ let org;
271
+ try {
272
+ org = ghJson(['api', `orgs/${owner}`], options);
273
+ } catch (error) {
274
+ if (isNotFound(error)) return FREE_PLAN;
275
+ throw error;
276
+ }
277
+
278
+ return (org && org.plan && org.plan.name) || FREE_PLAN;
279
+ }
280
+
281
+ module.exports = {
282
+ gh,
283
+ getRepo,
284
+ ensureRepo,
285
+ getPages,
286
+ ensurePages,
287
+ ownerPlan,
288
+ DEFAULT_PAGES_BRANCH,
289
+ FREE_PLAN,
290
+ };
@@ -1548,6 +1548,34 @@ function healFailedMessage(result) {
1548
1548
  ].join('\n');
1549
1549
  }
1550
1550
 
1551
+ /**
1552
+ * Leave the #350 prepare gate wired on a package this process just prepared
1553
+ * ([#870](https://github.com/Omega-JS-Stack/omega/issues/870)). prepare-package
1554
+ * rewrites `scripts.prepare` to its own UNGUARDED string on the way through, and
1555
+ * a heal that left it there hands a consumer install the door back. The prepare
1556
+ * script restores it in a `finally`, so this is the net for the case a finally
1557
+ * cannot reach: a prepare killed outright, or one that never ran the gate at all.
1558
+ * The guard is loaded from the devkit BESIDE the prepared package, the same hop
1559
+ * the gated script's own relative require makes, and its path is built at runtime
1560
+ * rather than written as a literal specifier, because devkit's vendored copies
1561
+ * carry `src/` only and a relative require of `tools/` would send the vendor walk
1562
+ * outside the module root.
1563
+ * @param {object} options
1564
+ * @param {string} options.monorepoRoot - The monorepo containing the package.
1565
+ * @param {string} options.packageDir - The package that was prepared.
1566
+ * @returns {boolean} Whether the manifest holds the gate now (true when the
1567
+ * checkout carries no guard to enforce).
1568
+ */
1569
+ function keepPrepareGuarded(options) {
1570
+ const { monorepoRoot, packageDir } = options;
1571
+ const guardPath = path.join(monorepoRoot, 'packages', 'devkit', 'tools', 'prepare-guard.js');
1572
+ if (!fs.existsSync(guardPath)) {
1573
+ return true;
1574
+ }
1575
+
1576
+ return require(guardPath).ensureGuarded({ packageDir }).guarded;
1577
+ }
1578
+
1551
1579
  /**
1552
1580
  * Check that a locally-linked framework's dist is at least as new as its src —
1553
1581
  * reporting or rebuilding it when a src edit landed without a prepare, so
@@ -1661,6 +1689,10 @@ function ensureFreshLocalDist(options) {
1661
1689
  stdio: 'inherit',
1662
1690
  shell: process.platform === 'win32',
1663
1691
  });
1692
+
1693
+ // Whatever that prepare did, it does not get to leave the gate off (#870)
1694
+ const guarded = inMonorepo ? keepPrepareGuarded({ monorepoRoot, packageDir: realDir }) : true;
1695
+
1664
1696
  if (result.status !== 0) {
1665
1697
  const failure = `npm run prepare exited ${result.status === null ? String(result.error && result.error.message || 'spawn error') : result.status}`;
1666
1698
  if (inMonorepo) {
@@ -1671,6 +1703,21 @@ function ensureFreshLocalDist(options) {
1671
1703
  console.warn(`omega: rebuild failed (${failure} in ${realDir}) — continuing on the stale dist`);
1672
1704
  return { status: 'rebuild-failed', packageName, dir: realDir };
1673
1705
  }
1706
+
1707
+ // A prepare that ended with the gate off hands a consumer install the #350
1708
+ // door back, so a heal that could not put it back is a FAILED heal like any
1709
+ // other: the boot stops on it rather than running on an ungated package.
1710
+ if (!guarded) {
1711
+ return {
1712
+ status: 'heal-failed',
1713
+ packageName,
1714
+ dir: realDir,
1715
+ reason,
1716
+ failure: 'npm run prepare left scripts.prepare off the #350 gate',
1717
+ monorepoRoot,
1718
+ };
1719
+ }
1720
+
1674
1721
  return { status: 'rebuilt', by: 'self', packageName, dir: realDir };
1675
1722
  });
1676
1723
  }
@@ -23,8 +23,16 @@
23
23
  // - The Default section is replaced with the new framework's defaults.
24
24
  // - Keys that already had values (in either Default or Custom) keep those values
25
25
  // in the same section they were in.
26
- // - Keys the user added to the Default section that are NOT in the new framework's
27
- // defaults migrate to the Custom section (so framework cleanups don't lose user data).
26
+ // - The Default section is COMPLETELY managed ([#926](https://github.com/Omega-JS-Stack/omega/issues/926)):
27
+ // a line the new framework block no longer carries LEAVES the consumer, so a
28
+ // rule the framework retires or rewrites actually takes effect everywhere. A
29
+ // retired framework line and a line a user typed inside the framework block
30
+ // cannot be told apart, and the block header already says it is overwritten on
31
+ // every setup, so `.gitignore` and `AGENTS.md` drop it. Lines a user owns
32
+ // belong under the Custom marker, which stays verbatim.
33
+ // - .env is the one exception: a retired key holding a NON-EMPTY value migrates
34
+ // to the Custom section (that value is data, not a rule), while an empty
35
+ // retired key (`KEY=` / `KEY=""`) drops like any other retired line.
28
36
  // - .env values are normalized to **double-quoted** form on every merge:
29
37
  // KEY=raw-value → KEY="raw-value"
30
38
  // KEY="already-quoted" → KEY="already-quoted" (left alone)
@@ -110,24 +118,23 @@ function mergeLineBasedFiles(existingContent, newContent, fileName) {
110
118
  }
111
119
  }
112
120
 
113
- // User-added stuff in their Default section that the new framework doesn't know about
114
- // → migrate to Custom so it's preserved without being clobbered next setup.
121
+ // Anything in the existing Default section the new framework block no longer
122
+ // carries. The block is COMPLETELY managed (#926), so it DROPS: a retired
123
+ // framework line and a line the user typed into the framework block cannot be
124
+ // told apart, and the block header already says it is overwritten on every
125
+ // setup. The one exception is an .env key holding a real value, which is the
126
+ // user's DATA rather than a rule, so it migrates down to Custom; an empty one
127
+ // drops with the rest. .gitignore and AGENTS.md migrate nothing.
115
128
  const migratedToCustom = [];
116
- for (const line of existingDefault) {
117
- const trimmed = line.trim();
118
- if (!trimmed || trimmed.startsWith('#')) continue;
129
+ if (isEnvFile) {
130
+ for (const line of existingDefault) {
131
+ const trimmed = line.trim();
132
+ if (!trimmed || trimmed.startsWith('#')) continue;
119
133
 
120
- if (isEnvFile) {
121
134
  const key = trimmed.split('=')[0].trim();
122
- if (key && !newDefaultKeys.has(key) && !existingCustomKeys.has(key)) {
135
+ if (key && !newDefaultKeys.has(key) && !existingCustomKeys.has(key) && !envValueIsEmpty(trimmed)) {
123
136
  migratedToCustom.push(normalizeEnvLine(line));
124
137
  }
125
- } else {
126
- // .gitignore: line not in new defaults → migrate.
127
- const inNew = newLines.some((nl) => nl.trim() === trimmed);
128
- if (!inNew) {
129
- migratedToCustom.push(line);
130
- }
131
138
  }
132
139
  }
133
140
 
@@ -186,7 +193,7 @@ function normalizeEnvLine(line) {
186
193
  if (eqIdx < 0) return line; // no `=` — not a KEY=VALUE line
187
194
 
188
195
  const key = trimmed.slice(0, eqIdx).trim();
189
- let value = trimmed.slice(eqIdx + 1);
196
+ const value = trimmed.slice(eqIdx + 1);
190
197
 
191
198
  // Strip trailing whitespace + inline comment-after-value (rare; we only strip a # that follows a space).
192
199
  // We do NOT strip # inside quoted values. Detect that by checking if value starts with a quote.
@@ -253,9 +253,10 @@ async function run({ hostName, hostRun, argv = process.argv.slice(2) }) {
253
253
  const target = findTarget(process.cwd());
254
254
 
255
255
  // No context — the bootstrap case (a verb run in a fresh directory has
256
- // no framework dep yet, by definition). Run the HOST framework's CLI, exactly
257
- // like the pre-dispatcher bins did, and say which one so a hoist-winner at a
258
- // brand root is never a silent mystery.
256
+ // no framework dep yet, by definition). Run the INSTALLED @omega.js/manager
257
+ // when one resolves, else the HOST framework's CLI exactly like the
258
+ // pre-dispatcher bins did, and say which one so a hoist-winner at a brand
259
+ // root is never a silent mystery.
259
260
  if (!target) {
260
261
  // …but ONLY for a verb that cannot write. The fallback hands a mutating verb
261
262
  // to whichever framework won npm's bin link, and that verb scaffolds its own
@@ -279,6 +280,20 @@ async function run({ hostName, hostRun, argv = process.argv.slice(2) }) {
279
280
  process.exit(1);
280
281
  }
281
282
 
283
+ // The manager wins a contextless run whenever the install carries one: the
284
+ // verbs that survive the refusal above are the ones that run where no
285
+ // target exists yet (onboard, create, new), and those are the manager's own.
286
+ // Without this, a fresh brand-template clone that installed ONLY
287
+ // @omega.js/manager still ran the backend, because the manager DEPENDS on
288
+ // the backend and npm's arbitrary hoist winner linked the backend's bin:
289
+ // `npx omega onboard` answered `Unknown command`
290
+ // ([#908](https://github.com/Omega-JS-Stack/omega/issues/908)).
291
+ const manager = tryResolveCli(MANAGER, process.cwd());
292
+ if (manager.cliPath) {
293
+ console.error(`omega: no target context found from ${process.cwd()}: running ${MANAGER}`);
294
+ return require(manager.cliPath).run();
295
+ }
296
+
282
297
  console.error(`omega: no target context found from ${process.cwd()} — running ${hostName}`);
283
298
  return hostRun();
284
299
  }