@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,118 @@
1
+ /**
2
+ * The ONE argv parse behind every OMEGA CLI: node's `util.parseArgs` wearing
3
+ * the yargs conveniences the bins and their commands still read
4
+ * ([#920](https://github.com/Omega-JS-Stack/omega/issues/920)). yargs is gone;
5
+ * this is what replaced it, in one place so the five CLIs can never drift.
6
+ *
7
+ * THE VALUE RULE, which is yargs' own: a flag named in `booleans` never takes a
8
+ * value, and every OTHER flag takes the next token as its value when that token
9
+ * exists and does not start with `-`, else it is `true`. `--flag=value` always
10
+ * wins. So a CLI declares only what is value-LESS, and a flag nobody declared
11
+ * still carries its value: the brand root forwards `omega test --filter foo` to
12
+ * a target whose parse has never heard of `--filter`, and CI really runs
13
+ * `sign-windows --in release --out release/signed`.
14
+ *
15
+ * What the builtin does NOT do, and what this adds back:
16
+ * 1. THE VALUE RULE above. To `parseArgs` an undeclared flag is boolean, so
17
+ * the value of a space-separated `--only hosting` would fall through to
18
+ * `_`. The line is PRE-SCANNED to build the options map instead: every
19
+ * flag whose occurrence takes a value is declared a string, and parseArgs
20
+ * still does all the tokenizing.
21
+ * 2. CAMEL TWINS. `parseArgs` gives `--dry-run` exactly one key, `dry-run`.
22
+ * Dozens of reads say `options.dryRun` (the manager alone has ~100) with
23
+ * no kebab fallback, and the manager's buildForwardedFlags skips a camel
24
+ * key whose kebab original exists. So every kebab flag lands under BOTH
25
+ * spellings, exactly as yargs minted them.
26
+ * 3. NEGATION. `parseArgs` reads `--no-secrets` as a flag NAMED `no-secrets`.
27
+ * Every consumer tests the positive key against false (`argv.https !==
28
+ * false`, the deploy precheck's `secrets: false`), so `--no-<x>` sets
29
+ * `<x>` to false, never takes a value, and never survives under its own
30
+ * name.
31
+ * 4. REPEATS. A repeated flag overwrites unless it is declared `multiple`,
32
+ * and an array of one is not what a single occurrence meant: the backend's
33
+ * `--where` accumulates into an array and one clause stays a string.
34
+ *
35
+ * Positionals come back at `_`, the key every consumer already reads. The
36
+ * router owns `--help`/`--version`, so nothing here is reserved.
37
+ *
38
+ * Stdlib-only on purpose: a vendored module must not push a dependency onto
39
+ * its hosts.
40
+ */
41
+
42
+ const { parseArgs } = require('node:util');
43
+
44
+ /** `signed-dir` becomes `signedDir` (the twin yargs minted beside every kebab key). */
45
+ function camelCase(name) {
46
+ return name.replace(/-([a-z0-9])/g, (match, character) => character.toUpperCase());
47
+ }
48
+
49
+ /**
50
+ * The options map for THIS line: the declarations plus every flag the value
51
+ * rule says carries a value. A clustered short (`-abc`) declares nothing and
52
+ * stays boolean, and so does a `--no-<x>` negation.
53
+ */
54
+ function declarationsFor(args, booleans, multiples) {
55
+ const options = {};
56
+
57
+ for (const name of booleans) options[name] = { type: 'boolean' };
58
+ for (const name of multiples) options[name] = { type: 'string', multiple: true };
59
+
60
+ for (let index = 0; index < args.length; index++) {
61
+ const token = args[index];
62
+ if (token === '--') break;
63
+
64
+ const flag = /^--([^-=][^=]*)(=)?/.exec(token) || /^-([^-])$/.exec(token);
65
+ if (!flag) continue;
66
+
67
+ const name = flag[1];
68
+ if (options[name]) continue;
69
+
70
+ // A negation is value-LESS (rule 3), so the token behind it is a positional
71
+ // of its own and never the negation's value.
72
+ if (name.startsWith('no-')) {
73
+ options[name] = { type: 'boolean' };
74
+ continue;
75
+ }
76
+
77
+ const next = args[index + 1];
78
+ if (flag[2] || (next !== undefined && !next.startsWith('-'))) {
79
+ options[name] = { type: 'string' };
80
+ }
81
+ }
82
+
83
+ return options;
84
+ }
85
+
86
+ /**
87
+ * Parse a CLI argv tail into the yargs-shaped options object the CLIs read.
88
+ *
89
+ * @param {string[]} args - The argv tail (`process.argv.slice(2)`).
90
+ * @param {object} [declarations]
91
+ * @param {string[]} [declarations.booleans] - Value-less flags (`--extended`), the only list a CLI owes
92
+ * @param {string[]} [declarations.multiples] - Value flags that accumulate when repeated (`--where`).
93
+ * @returns {object} `{ _: positionals, ...flags }`, each kebab flag under both spellings
94
+ */
95
+ function parseArgv(args, declarations = {}) {
96
+ const { booleans = [], multiples = [] } = declarations;
97
+ const options = declarationsFor(args, booleans, multiples);
98
+
99
+ const { values, positionals } = parseArgs({ args, options, strict: false, allowPositionals: true });
100
+ const parsed = { _: positionals };
101
+
102
+ for (const [key, raw] of Object.entries(values)) {
103
+ // A repeated declaration always answers an array; one occurrence is one value.
104
+ const value = Array.isArray(raw) && raw.length === 1 ? raw[0] : raw;
105
+
106
+ // Negation names the POSITIVE flag. Last spelling wins, as it did under
107
+ // yargs: `--secrets --no-secrets` ends false.
108
+ const negated = key.startsWith('no-');
109
+ const name = negated ? key.slice(3) : key;
110
+
111
+ parsed[name] = negated ? false : value;
112
+ if (name.includes('-')) parsed[camelCase(name)] = parsed[name];
113
+ }
114
+
115
+ return parsed;
116
+ }
117
+
118
+ module.exports = { parseArgv };
@@ -22,6 +22,11 @@
22
22
  // That is what keeps an inner tee's detach from stealing (and truncating) the outer tee's
23
23
  // file, the way a shared singleton did.
24
24
  //
25
+ // Two attaches on ONE tee stack the same way: a second attach of a DIFFERENT path pushes a
26
+ // layer on top of the first instead of replacing it, so a verb that runs another verb in
27
+ // process (`omega test` running `omega deploy`) keeps teeing both files. Detach order is
28
+ // LIFO, and the module-level `detach()` pops the newest layer.
29
+ //
25
30
  // Skipped in CI: a runner has its own log capture and wants no logs/ left in the workspace.
26
31
  // `attachInCI: true` opts out of that skip, for a sink whose file is the POINT rather than
27
32
  // workspace debris (the signing box's own runner log, which a CI job is exactly when it
@@ -51,8 +56,10 @@ function isCI(env) {
51
56
 
52
57
  // Factory — each call returns an independent tee with its own closure state.
53
58
  function createTee() {
54
- let activePath = null;
55
- let activeDetach = null;
59
+ // One entry per live attach on this tee, oldest first. A layer restores the writers it
60
+ // captured, so detaching in LIFO order (the only order a nested run produces) unwinds
61
+ // the patches exactly as they went on.
62
+ const layers = [];
56
63
  let warned = false;
57
64
 
58
65
  function attach(filePath, options) {
@@ -63,10 +70,10 @@ function createTee() {
63
70
 
64
71
  const abs = path.resolve(filePath);
65
72
 
66
- // Already teeing this exact file — do not patch a second time (that would write
67
- // every line twice).
68
- if (activePath === abs) { return activeDetach; }
69
- if (activeDetach) { activeDetach(); }
73
+ // Already teeing this exact file, so do not patch a second time (that would write every
74
+ // line twice, and reopen the file truncated under the live layer).
75
+ const live = layers.find((layer) => layer.path === abs);
76
+ if (live) { return live.detach; }
70
77
 
71
78
  let fd;
72
79
  try {
@@ -125,21 +132,22 @@ function createTee() {
125
132
  process.off('exit', closeFd);
126
133
  closeFd();
127
134
 
128
- if (activeDetach === detach) {
129
- activePath = null;
130
- activeDetach = null;
131
- }
135
+ const index = layers.indexOf(layer);
136
+ if (index !== -1) { layers.splice(index, 1); }
132
137
  }
133
138
 
134
- activePath = abs;
135
- activeDetach = detach;
139
+ const layer = { path: abs, detach };
140
+ layers.push(layer);
136
141
 
137
142
  return detach;
138
143
  }
139
144
 
140
145
  return {
141
146
  attach,
142
- detach: () => { if (activeDetach) { activeDetach(); } },
147
+ detach: () => {
148
+ const newest = layers[layers.length - 1];
149
+ if (newest) { newest.detach(); }
150
+ },
143
151
  };
144
152
  }
145
153
 
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Brand accent ramp — derives the accent token family from ONE brand color
3
+ * (C3/D6: `brand.color` drives the accent everywhere via CSS custom
4
+ * properties; the static token sheet's neutral placeholder stands when the
5
+ * brand has no usable color).
6
+ *
7
+ * Pure math, dependency-free: hex → RGB → HSL round-trips plus WCAG
8
+ * relative luminance for the on-accent ink pick. Every output value is
9
+ * regenerated from parsed numbers, so nothing from config reaches the
10
+ * emitted <style> block verbatim.
11
+ *
12
+ * ONE home for three surfaces ([#912](https://github.com/Omega-JS-Stack/omega/issues/912)):
13
+ * @omega.js/web inlines the ramp into <head> after its bundles, and
14
+ * @omega.js/desktop and @omega.js/extension write `renderBrandScss()` below to
15
+ * a generated `_brand.scss` their sass tasks compile. Same hex in, same ramp
16
+ * out, on every target.
17
+ */
18
+
19
+ const INK_DARK = '#111213';
20
+ const INK_LIGHT = '#ffffff';
21
+
22
+ // WCAG contrast-vs-white beats contrast-vs-black below this luminance
23
+ const INK_LUMINANCE_THRESHOLD = 0.1791;
24
+
25
+ // Accents darker than this lightness brighten on hover instead of darkening
26
+ const HOVER_LIGHTEN_THRESHOLD = 0.25;
27
+
28
+ // Dark-mode variant: accents at/above this lightness are already legible on
29
+ // charcoal and pass through; darker ones lift into the floor..ceil band.
30
+ const DARK_KEEP_THRESHOLD = 0.66;
31
+ const DARK_LIGHTNESS_FLOOR = 0.55;
32
+ const DARK_LIGHTNESS_CEIL = 0.78;
33
+
34
+ /**
35
+ * Parse a 3- or 6-digit hex color.
36
+ * @param {*} value - candidate color
37
+ * @returns {{ r: number, g: number, b: number }|null} channels 0-255, or null
38
+ */
39
+ function parseHex(value) {
40
+ if (typeof value !== 'string') {
41
+ return null;
42
+ }
43
+
44
+ const match = value.trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
45
+ if (!match) {
46
+ return null;
47
+ }
48
+
49
+ let hex = match[1].toLowerCase();
50
+ if (hex.length === 3) {
51
+ hex = hex.split('').map((c) => c + c).join('');
52
+ }
53
+
54
+ return {
55
+ r: parseInt(hex.slice(0, 2), 16),
56
+ g: parseInt(hex.slice(2, 4), 16),
57
+ b: parseInt(hex.slice(4, 6), 16),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * WCAG 2.x relative luminance.
63
+ * @param {{ r: number, g: number, b: number }} rgb - channels 0-255
64
+ * @returns {number} 0 (black) to 1 (white)
65
+ */
66
+ function relativeLuminance({ r, g, b }) {
67
+ const linearize = (channel) => {
68
+ const c = channel / 255;
69
+ return c <= 0.03928 ? c / 12.92 : (((c + 0.055) / 1.055) ** 2.4);
70
+ };
71
+ return (0.2126 * linearize(r)) + (0.7152 * linearize(g)) + (0.0722 * linearize(b));
72
+ }
73
+
74
+ /**
75
+ * RGB (0-255) → HSL (h 0-360, s/l 0-1).
76
+ */
77
+ function rgbToHsl({ r, g, b }) {
78
+ const rn = r / 255;
79
+ const gn = g / 255;
80
+ const bn = b / 255;
81
+ const max = Math.max(rn, gn, bn);
82
+ const min = Math.min(rn, gn, bn);
83
+ const l = (max + min) / 2;
84
+
85
+ if (max === min) {
86
+ return { h: 0, s: 0, l };
87
+ }
88
+
89
+ const d = max - min;
90
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
91
+ let h;
92
+ if (max === rn) {
93
+ h = ((gn - bn) / d) + (gn < bn ? 6 : 0);
94
+ } else if (max === gn) {
95
+ h = ((bn - rn) / d) + 2;
96
+ } else {
97
+ h = ((rn - gn) / d) + 4;
98
+ }
99
+
100
+ return { h: h * 60, s, l };
101
+ }
102
+
103
+ /**
104
+ * HSL (h 0-360, s/l 0-1) → RGB (0-255).
105
+ */
106
+ function hslToRgb({ h, s, l }) {
107
+ if (s === 0) {
108
+ const v = Math.round(l * 255);
109
+ return { r: v, g: v, b: v };
110
+ }
111
+
112
+ const hueToChannel = (p, q, t) => {
113
+ let tn = t;
114
+ if (tn < 0) tn += 1;
115
+ if (tn > 1) tn -= 1;
116
+ if (tn < 1 / 6) return p + ((q - p) * 6 * tn);
117
+ if (tn < 1 / 2) return q;
118
+ if (tn < 2 / 3) return p + ((q - p) * ((2 / 3) - tn) * 6);
119
+ return p;
120
+ };
121
+
122
+ const q = l < 0.5 ? l * (1 + s) : l + s - (l * s);
123
+ const p = (2 * l) - q;
124
+ const hn = h / 360;
125
+
126
+ return {
127
+ r: Math.round(hueToChannel(p, q, hn + (1 / 3)) * 255),
128
+ g: Math.round(hueToChannel(p, q, hn) * 255),
129
+ b: Math.round(hueToChannel(p, q, hn - (1 / 3)) * 255),
130
+ };
131
+ }
132
+
133
+ /**
134
+ * RGB → #rrggbb.
135
+ */
136
+ function toHex({ r, g, b }) {
137
+ const pair = (v) => v.toString(16).padStart(2, '0');
138
+ return `#${pair(r)}${pair(g)}${pair(b)}`;
139
+ }
140
+
141
+ /**
142
+ * Shift an accent's lightness for hover/active states — darker accents
143
+ * brighten, everything else deepens, clamped to [0, 1].
144
+ * @param {{ h: number, s: number, l: number }} hsl - base accent
145
+ * @param {number} amount - lightness delta (positive magnitude)
146
+ * @returns {string} #rrggbb
147
+ */
148
+ function shiftLightness(hsl, amount) {
149
+ const direction = hsl.l < HOVER_LIGHTEN_THRESHOLD ? 1 : -1;
150
+ const l = Math.min(1, Math.max(0, hsl.l + (direction * amount)));
151
+ return toHex(hslToRgb({ ...hsl, l }));
152
+ }
153
+
154
+ /**
155
+ * Compose one accent ramp from resolved channels.
156
+ * @param {{ r: number, g: number, b: number }} rgb - accent channels
157
+ * @param {{ h: number, s: number, l: number }} hsl - same accent in HSL
158
+ * @param {number} subtleAlpha - alpha for the subtle wash (mode-specific)
159
+ * @returns {object} { accent, accentHover, accentActive, accentSubtle, accentInk, accentRing }
160
+ */
161
+ function composeRamp(rgb, hsl, subtleAlpha) {
162
+ const luminance = relativeLuminance(rgb);
163
+
164
+ return {
165
+ accent: toHex(rgb),
166
+ accentHover: shiftLightness(hsl, 0.07),
167
+ accentActive: shiftLightness(hsl, 0.11),
168
+ accentSubtle: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${subtleAlpha})`,
169
+ accentInk: luminance > INK_LUMINANCE_THRESHOLD ? INK_DARK : INK_LIGHT,
170
+ accentRing: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.35)`,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Compose the accent token families from a brand color: the light ramp plus
176
+ * a dark-mode variant (darker brand colors lift so they stay legible on the
177
+ * charcoal ground — the classy v2 per-mode accent).
178
+ * @param {string} color - brand color (#rgb or #rrggbb)
179
+ * @returns {object|null} ramps for the head emitter, or null when unusable:
180
+ * { accent, accentHover, accentActive, accentSubtle, accentInk, accentRing,
181
+ * dark: { …same shape… } }
182
+ */
183
+ function composeBrandTokens(color) {
184
+ const rgb = parseHex(color);
185
+ if (!rgb) {
186
+ return null;
187
+ }
188
+
189
+ const hsl = rgbToHsl(rgb);
190
+ const darkHsl = hsl.l >= DARK_KEEP_THRESHOLD
191
+ ? hsl
192
+ : { ...hsl, l: Math.min(Math.max(hsl.l + 0.1, DARK_LIGHTNESS_FLOOR), DARK_LIGHTNESS_CEIL) };
193
+ const darkRgb = hslToRgb(darkHsl);
194
+
195
+ return {
196
+ ...composeRamp(rgb, hsl, 0.12),
197
+ dark: composeRamp(darkRgb, darkHsl, 0.16),
198
+ };
199
+ }
200
+
201
+ /**
202
+ * The ONE accent default. `brand.color` unset (or unusable) lands here, and the
203
+ * classy theme's `$primary: … !default` carries the same hex purely as the Sass
204
+ * mechanism that lets this partial win: the VALUE is decided here.
205
+ */
206
+ const DEFAULT_BRAND_COLOR = '#2563EB';
207
+
208
+ /** The six properties, in the order head.html emits them on web. */
209
+ const RAMP_PROPERTIES = [
210
+ ['--omega-accent', 'accent'],
211
+ ['--omega-accent-hover', 'accentHover'],
212
+ ['--omega-accent-active', 'accentActive'],
213
+ ['--omega-accent-subtle', 'accentSubtle'],
214
+ ['--omega-accent-ink', 'accentInk'],
215
+ ['--omega-accent-ring', 'accentRing'],
216
+ ];
217
+
218
+ /**
219
+ * One `selector { … }` block of the ramp, indented for the mixin body.
220
+ * @param {string} selector - the rule's selector
221
+ * @param {object} ramp - one ramp (the light half, or `.dark`)
222
+ * @param {number} depth - indent depth in 2-space steps
223
+ * @returns {string} the rule text
224
+ */
225
+ function renderRampBlock(selector, ramp, depth) {
226
+ const pad = ' '.repeat(depth);
227
+ const declarations = RAMP_PROPERTIES.map(([property, key]) => `${pad} ${property}: ${ramp[key]};`);
228
+
229
+ return [`${pad}${selector} {`, ...declarations, `${pad}}`].join('\n');
230
+ }
231
+
232
+ /**
233
+ * Render the generated `_brand.scss` a target's sass task writes beside its
234
+ * compile: `$primary` for the compile-time accent Bootstrap derives from, plus
235
+ * a `ramp` mixin carrying the runtime `--omega-accent` family under the SAME
236
+ * three stamps the token sheet uses (bare `:root`, the OS preference, the
237
+ * explicit `data-bs-theme` stamp both ways).
238
+ *
239
+ * The css rides a MIXIN rather than the file root on purpose: a used module's
240
+ * css is emitted at its LOAD position, which is ahead of the framework module's
241
+ * token sheet and theme, so a root-level rule here would lose the cascade to
242
+ * the very placeholders it replaces. The consumer's `@include brand.ramp;` puts
243
+ * it after both.
244
+ *
245
+ * @param {object|null} tokens - composeBrandTokens() output, or null for the default
246
+ * @returns {string} the partial's full text
247
+ */
248
+ function renderBrandScss(tokens) {
249
+ const ramp = tokens || composeBrandTokens(DEFAULT_BRAND_COLOR);
250
+
251
+ return `// AUTO-GENERATED by the sass task from \`brand.color\` in omega.json5.
252
+ // Every build rewrites it: edit the config, not this file.
253
+ //
254
+ // \`$primary\` is the compile-time accent (Bootstrap's color ramp compiles from
255
+ // it) and \`ramp\` is the runtime --omega-accent family, the same light and dark
256
+ // ramps @omega.js/web inlines into <head>. Include the mixin AFTER the
257
+ // framework module so the ramp wins the cascade over the token sheet's
258
+ // placeholders.
259
+
260
+ $primary: ${ramp.accent};
261
+
262
+ @mixin ramp {
263
+ ${renderRampBlock(':root', ramp, 1)}
264
+
265
+ // The OS preference carries when no explicit choice is stamped...
266
+ @media (prefers-color-scheme: dark) {
267
+ ${renderRampBlock(':root', ramp.dark, 2)}
268
+ }
269
+
270
+ // ...and the appearance stamp beats the OS in BOTH directions.
271
+ ${renderRampBlock(":root[data-bs-theme='dark']", ramp.dark, 1)}
272
+
273
+ ${renderRampBlock(":root[data-bs-theme='light']", ramp, 1)}
274
+ }
275
+ `;
276
+ }
277
+
278
+ module.exports = { composeBrandTokens, parseHex, relativeLuminance, renderBrandScss, DEFAULT_BRAND_COLOR };