@omega.js/desktop 0.52.0 → 0.54.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 (345) hide show
  1. package/README.md +53 -48
  2. package/dist/assets/css/core/_initialize.scss +1 -1
  3. package/dist/assets/js/core/app-shell.js +14 -15
  4. package/dist/assets/themes/_template/_theme.js +6 -6
  5. package/dist/assets/themes/base/_includes/global/sections/account.html +4 -4
  6. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +5 -5
  7. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +3 -3
  8. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +1 -1
  9. package/dist/assets/themes/base/_layouts/frontend/pages/blog/tags/tag.html +1 -1
  10. package/dist/assets/themes/base/_layouts/frontend/pages/payment/confirmation.html +1 -1
  11. package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.js +2 -3
  12. package/dist/assets/themes/base/_sections/verts/unit/section.html +1 -1
  13. package/dist/assets/themes/base/_sections/verts/unit/section.js +3 -4
  14. package/dist/assets/themes/base/_theme.js +4 -3
  15. package/dist/assets/themes/bootstrap/_theme.js +2 -2
  16. package/dist/assets/themes/bootstrap/overrides/_links.scss +1 -1
  17. package/dist/assets/themes/classy/_theme.js +8 -6
  18. package/dist/assets/themes/classy/css/marketing/_sections.scss +1 -2
  19. package/dist/assets/themes/classy/js/hero-demo-form.js +3 -2
  20. package/dist/assets/themes/neobrutalism/_theme.js +5 -5
  21. package/dist/assets/themes/neobrutalism/js/pages/test/libraries/layers/index.js +1 -1
  22. package/dist/assets/themes/newsflash/_theme.js +6 -6
  23. package/dist/assets/themes/newsflash/js/pages/test/libraries/layers/index.js +1 -1
  24. package/dist/build.js +87 -111
  25. package/dist/cli-run.js +4 -1
  26. package/dist/cli.js +3 -3
  27. package/dist/commands/build.js +2 -2
  28. package/dist/commands/cdp/capture.js +2 -2
  29. package/dist/commands/cdp/client.js +1 -1
  30. package/dist/commands/cdp/quit.js +2 -2
  31. package/dist/commands/cdp/relaunch.js +2 -2
  32. package/dist/commands/cdp/theme.js +1 -1
  33. package/dist/commands/cdp.js +1 -1
  34. package/dist/commands/clean.js +4 -5
  35. package/dist/commands/deploy.js +4 -4
  36. package/dist/commands/dev.js +25 -0
  37. package/dist/commands/finalize-release.js +4 -4
  38. package/dist/commands/launch.js +2 -2
  39. package/dist/commands/lib/deploy-precheck.js +3 -3
  40. package/dist/commands/lib/ensure-target.js +18 -23
  41. package/dist/commands/lib/migrate.js +17 -0
  42. package/dist/commands/logs.js +1 -1
  43. package/dist/commands/package.js +2 -2
  44. package/dist/commands/publish.js +2 -2
  45. package/dist/commands/release.js +4 -4
  46. package/dist/commands/runner.js +11 -11
  47. package/dist/commands/sign-windows.js +5 -5
  48. package/dist/commands/test.js +8 -8
  49. package/dist/commands/update.js +7 -6
  50. package/dist/commands/validate-certs.js +4 -4
  51. package/dist/commands/version.js +3 -3
  52. package/dist/defaults/.github/workflows/build.yml +18 -18
  53. package/dist/defaults/_.gitignore +0 -2
  54. package/dist/defaults/_mas/README.md +3 -3
  55. package/dist/defaults/config/certs/README.md +1 -1
  56. package/dist/defaults/config/omega.json5 +43 -43
  57. package/dist/defaults/docs/README.md +3 -3
  58. package/dist/defaults/gulpfile.js +1 -1
  59. package/dist/defaults/hooks/build/post.js +2 -2
  60. package/dist/defaults/hooks/build/pre.js +2 -2
  61. package/dist/defaults/hooks/deploy/pre.js +1 -1
  62. package/dist/defaults/hooks/notarize/post.js +2 -2
  63. package/dist/defaults/hooks/release/post.js +2 -2
  64. package/dist/defaults/hooks/release/pre.js +2 -2
  65. package/dist/defaults/src/assets/js/components/about/index.js +3 -5
  66. package/dist/defaults/src/assets/js/components/main/index.js +3 -5
  67. package/dist/defaults/src/assets/js/components/settings/index.js +3 -5
  68. package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
  69. package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
  70. package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
  71. package/dist/defaults/src/integrations/context-menu/index.js +13 -13
  72. package/dist/defaults/src/integrations/menu/index.js +8 -8
  73. package/dist/defaults/src/integrations/tray/index.js +12 -12
  74. package/dist/defaults/src/main.js +5 -7
  75. package/dist/defaults/src/preload.js +4 -6
  76. package/dist/defaults/test/README.md +5 -5
  77. package/dist/defaults/test/_init.js +1 -1
  78. package/dist/gulp/main.js +9 -10
  79. package/dist/gulp/tasks/audit.js +11 -14
  80. package/dist/gulp/tasks/build-config.js +8 -8
  81. package/dist/gulp/tasks/bundle.js +16 -16
  82. package/dist/gulp/tasks/defaults.js +3 -3
  83. package/dist/gulp/tasks/distribute.js +2 -2
  84. package/dist/gulp/tasks/html.js +9 -9
  85. package/dist/gulp/tasks/package-quick.js +3 -3
  86. package/dist/gulp/tasks/package.js +3 -3
  87. package/dist/gulp/tasks/release.js +3 -3
  88. package/dist/gulp/tasks/sass.js +6 -6
  89. package/dist/gulp/tasks/serve.js +4 -4
  90. package/dist/hooks/notarize-artifacts.js +1 -1
  91. package/dist/hooks/notarize.js +1 -1
  92. package/dist/index.js +5 -8
  93. package/dist/lib/_environment-mixin.js +50 -0
  94. package/dist/lib/_lifecycle-mixin.js +45 -0
  95. package/dist/lib/analytics.js +33 -35
  96. package/dist/lib/app-state.js +14 -14
  97. package/dist/lib/auth-flow.js +18 -18
  98. package/dist/lib/auth-persistence.js +12 -12
  99. package/dist/lib/auth.js +421 -0
  100. package/dist/lib/auto-updater.js +49 -49
  101. package/dist/lib/context-menu.js +13 -13
  102. package/dist/lib/context.js +19 -19
  103. package/dist/lib/deep-link.js +34 -34
  104. package/dist/lib/fontawesome.js +5 -5
  105. package/dist/lib/ipc.js +4 -4
  106. package/dist/lib/menu.js +25 -25
  107. package/dist/lib/protocol.js +5 -5
  108. package/dist/lib/remote-config.js +22 -22
  109. package/dist/lib/remote-scripts.js +21 -21
  110. package/dist/lib/restart-manager/index.js +29 -29
  111. package/dist/lib/restart-manager/install.js +1 -1
  112. package/dist/lib/restart-manager/protocol.js +1 -1
  113. package/dist/lib/sign-helpers/exec-with-limit.js +1 -1
  114. package/dist/lib/sign-helpers/sign-events.js +1 -1
  115. package/dist/lib/startup.js +18 -13
  116. package/dist/lib/storage.js +10 -10
  117. package/dist/lib/templating.js +16 -16
  118. package/dist/lib/theme.js +10 -10
  119. package/dist/lib/tray.js +27 -27
  120. package/dist/lib/usage.js +11 -11
  121. package/dist/lib/window-manager.js +26 -26
  122. package/dist/main.js +399 -483
  123. package/dist/preload.js +236 -176
  124. package/dist/renderer.js +417 -391
  125. package/dist/test/fixtures/consumer-app/config/omega.json5 +1 -1
  126. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +8 -11
  127. package/dist/test/fixtures/consumer-app/src/main.js +5 -7
  128. package/dist/test/fixtures/consumer-app/src/preload.js +2 -2
  129. package/dist/test/harness/boot-entry.js +22 -20
  130. package/dist/test/harness/main-entry.js +31 -30
  131. package/dist/test/harness/renderer-entry.js +5 -5
  132. package/dist/test/harness/renderer-preload.js +137 -141
  133. package/dist/test/index.js +10 -10
  134. package/dist/test/runner.js +2 -2
  135. package/dist/test/runners/boot.js +7 -6
  136. package/dist/test/runners/electron.js +3 -2
  137. package/dist/test/runners/render-event.js +2 -2
  138. package/dist/test/suites/boot/consumer-app-boots.test.js +57 -26
  139. package/dist/test/suites/boot/restart-manager.test.js +2 -2
  140. package/dist/test/suites/boot/storage-bundled.test.js +5 -5
  141. package/dist/test/suites/boot/theme.test.js +13 -13
  142. package/dist/test/suites/build/audit.test.js +21 -8
  143. package/dist/test/suites/build/auth-persistence-resolve.test.js +6 -6
  144. package/dist/test/suites/build/boot-fixture.test.js +2 -2
  145. package/dist/test/suites/build/boot-runner-timeout.test.js +6 -5
  146. package/dist/test/suites/build/brand-scss.test.js +1 -1
  147. package/dist/test/suites/build/build-json-bake.test.js +1 -1
  148. package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
  149. package/dist/test/suites/build/cli.test.js +30 -2
  150. package/dist/test/suites/build/config-schema.test.js +5 -5
  151. package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
  152. package/dist/test/suites/build/defaults-scaffold.test.js +21 -7
  153. package/dist/test/suites/build/deploy-direct.test.js +7 -5
  154. package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
  155. package/dist/test/suites/build/deploy-hook.test.js +7 -5
  156. package/dist/test/suites/build/dev-verb.test.js +67 -0
  157. package/dist/test/suites/build/ensure-target.test.js +13 -5
  158. package/dist/test/suites/build/env-delivery.test.js +2 -2
  159. package/dist/test/suites/build/esm-only-dependency.test.js +2 -2
  160. package/dist/test/suites/build/exports.test.js +9 -8
  161. package/dist/test/suites/build/get-config.test.js +4 -4
  162. package/dist/test/suites/build/manifest-deps.test.js +1 -1
  163. package/dist/test/suites/build/merge-line-files.test.js +7 -7
  164. package/dist/test/suites/build/migrate.test.js +29 -0
  165. package/dist/test/suites/build/omega-shell.test.js +34 -2
  166. package/dist/test/suites/build/omega.test.js +350 -0
  167. package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
  168. package/dist/test/suites/build/renderer-auth-bridge.test.js +211 -78
  169. package/dist/test/suites/build/runner-env-write.test.js +73 -0
  170. package/dist/test/suites/build/runner.test.js +11 -10
  171. package/dist/test/suites/build/sentry.test.js +2 -2
  172. package/dist/test/suites/build/setup-scripts.test.js +27 -0
  173. package/dist/test/suites/build/sign-windows-e2e.test.js +2 -2
  174. package/dist/test/suites/build/templating.test.js +3 -3
  175. package/dist/test/suites/build/test-stealth.test.js +7 -9
  176. package/dist/test/suites/build/url-helpers.test.js +55 -56
  177. package/dist/test/suites/build/validate-config.test.js +15 -4
  178. package/dist/test/suites/build/verb-logs.test.js +20 -0
  179. package/dist/test/suites/build/wave5-pins.test.js +2 -2
  180. package/dist/test/suites/main/analytics.test.js +59 -59
  181. package/dist/test/suites/main/app-state.test.js +66 -66
  182. package/dist/test/suites/main/auth-flow.test.js +41 -41
  183. package/dist/test/suites/main/auth-persistence.test.js +54 -43
  184. package/dist/test/suites/main/{client-bridge.integration.test.js → auth.integration.test.js} +10 -9
  185. package/dist/test/suites/main/auth.test.js +336 -0
  186. package/dist/test/suites/main/auto-updater.test.js +134 -134
  187. package/dist/test/suites/main/boot-sequence.test.js +37 -49
  188. package/dist/test/suites/main/context-menu.test.js +51 -50
  189. package/dist/test/suites/main/context.test.js +25 -25
  190. package/dist/test/suites/main/deep-link.test.js +74 -74
  191. package/dist/test/suites/main/fontawesome.test.js +27 -27
  192. package/dist/test/suites/main/ipc.test.js +35 -35
  193. package/dist/test/suites/main/menu.test.js +101 -100
  194. package/dist/test/suites/main/protocol.test.js +19 -19
  195. package/dist/test/suites/main/remote-config.test.js +63 -63
  196. package/dist/test/suites/main/remote-scripts.test.js +103 -103
  197. package/dist/test/suites/main/request.test.js +71 -0
  198. package/dist/test/suites/main/restart-manager.test.js +33 -33
  199. package/dist/test/suites/main/startup-paths-and-ua.test.js +4 -4
  200. package/dist/test/suites/main/startup.test.js +26 -26
  201. package/dist/test/suites/main/stealth-window.test.js +1 -1
  202. package/dist/test/suites/main/storage.test.js +25 -25
  203. package/dist/test/suites/main/theme.test.js +34 -34
  204. package/dist/test/suites/main/tray.test.js +79 -79
  205. package/dist/test/suites/main/url-helpers.test.js +133 -133
  206. package/dist/test/suites/main/usage.test.js +25 -25
  207. package/dist/test/suites/main/window-bounds.test.js +27 -27
  208. package/dist/test/suites/main/window-manager.test.js +44 -44
  209. package/dist/test/suites/renderer/analytics-bridge.test.js +5 -5
  210. package/dist/test/suites/renderer/cross-context-helpers.test.js +37 -31
  211. package/dist/test/suites/renderer/round-trip.test.js +3 -3
  212. package/dist/test/suites/renderer/tooltips.test.js +15 -15
  213. package/dist/test/suites/renderer/{window-em-surface.test.js → window-desktop-surface.test.js} +28 -8
  214. package/dist/test/utils/extended-mode-warning.js +1 -1
  215. package/dist/utils/boot-harness.js +56 -0
  216. package/dist/utils/build-pipeline.js +4 -4
  217. package/dist/utils/mode-helpers.js +2 -15
  218. package/dist/utils/runner-env.js +13 -28
  219. package/dist/utils/ship-keys.js +3 -3
  220. package/dist/utils/signing-status.js +51 -0
  221. package/dist/utils/test-events.js +7 -0
  222. package/dist/utils/test-stealth.js +6 -6
  223. package/dist/utils/url-helpers.js +52 -42
  224. package/dist/utils/user-agent.js +44 -0
  225. package/dist/vendor/account/engine.js +3 -3
  226. package/dist/vendor/account/index.js +14 -45
  227. package/dist/vendor/account/resolve-account.js +44 -0
  228. package/dist/vendor/account/schema.js +1 -1
  229. package/dist/vendor/account/user.js +99 -0
  230. package/dist/vendor/config/client-config.js +1 -1
  231. package/dist/vendor/config/company.js +46 -14
  232. package/dist/vendor/config/defaults.js +30 -7
  233. package/dist/vendor/config/edit.js +25 -3
  234. package/dist/vendor/config/env-delivery.js +1 -1
  235. package/dist/vendor/config/env-schema.js +3 -6
  236. package/dist/vendor/config/env.js +34 -22
  237. package/dist/vendor/config/environment.js +11 -30
  238. package/dist/vendor/config/index.js +21 -28
  239. package/dist/vendor/config/load.js +16 -9
  240. package/dist/vendor/config/platforms.js +1 -1
  241. package/dist/vendor/config/repo.js +10 -27
  242. package/dist/vendor/config/schema-client.js +64 -0
  243. package/dist/vendor/config/schema-cloud.js +38 -0
  244. package/dist/vendor/config/schema-manager.js +118 -0
  245. package/dist/vendor/config/schema-overrides.js +68 -0
  246. package/dist/vendor/config/schema.js +104 -160
  247. package/dist/vendor/config/site-global.js +2 -3
  248. package/dist/vendor/config/validate.js +97 -78
  249. package/dist/vendor/config/winback.js +1 -1
  250. package/dist/vendor/devkit/actions-secrets.js +1 -1
  251. package/dist/vendor/devkit/agents-md.js +233 -0
  252. package/dist/vendor/devkit/attach-log-file.js +16 -2
  253. package/dist/vendor/devkit/build-json.js +1 -1
  254. package/dist/vendor/devkit/ci-workflows.js +30 -30
  255. package/dist/vendor/devkit/cli-router.js +16 -11
  256. package/dist/vendor/devkit/defaults-engine.js +15 -51
  257. package/dist/vendor/devkit/deploy-snapshot.js +44 -9
  258. package/dist/vendor/devkit/env-lines.js +183 -0
  259. package/dist/vendor/devkit/local.js +64 -10
  260. package/dist/vendor/devkit/lockfile.js +32 -13
  261. package/dist/vendor/devkit/logger.js +7 -2
  262. package/dist/vendor/devkit/merge-line-files.js +219 -177
  263. package/dist/vendor/devkit/omega-bin.js +208 -111
  264. package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
  265. package/dist/vendor/devkit/preludes/index.js +1 -0
  266. package/dist/vendor/devkit/target-picker.js +45 -0
  267. package/dist/vendor/devkit/test/dashed-files.js +37 -0
  268. package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
  269. package/dist/vendor/devkit/test/runner-core.js +6 -6
  270. package/dist/vendor/devkit/update.js +15 -15
  271. package/dist/vendor/devkit/verb-scripts.js +40 -0
  272. package/dist/vendor/devkit/verbs.js +170 -0
  273. package/dist/vendor/monitoring/env.js +2 -2
  274. package/dist/vendor/monitoring/index.js +1 -1
  275. package/dist/vendor/monitoring/main.js +1 -1
  276. package/dist/vendor/monitoring/preload.js +1 -1
  277. package/dist/vendor/monitoring/renderer.js +1 -1
  278. package/package.json +19 -26
  279. package/dist/commands/install.js +0 -37
  280. package/dist/defaults/AGENTS.md +0 -110
  281. package/dist/defaults/CLAUDE.md +0 -1
  282. package/dist/lib/client-bridge.js +0 -374
  283. package/dist/lib/logger.js +0 -4
  284. package/dist/test/suites/build/manager.test.js +0 -213
  285. package/dist/test/suites/main/client-bridge.test.js +0 -262
  286. package/dist/vendor/config/env-retired.js +0 -137
  287. package/dist/vendor/config/retired-keys.js +0 -635
  288. package/docs/analytics.md +0 -140
  289. package/docs/app-state.md +0 -92
  290. package/docs/audit.md +0 -69
  291. package/docs/auto-updater.md +0 -243
  292. package/docs/boot-sequence.md +0 -39
  293. package/docs/build-system.md +0 -169
  294. package/docs/cdp-debugging.md +0 -169
  295. package/docs/client-bridge.md +0 -269
  296. package/docs/common-mistakes.md +0 -21
  297. package/docs/config-schema.md +0 -120
  298. package/docs/context-menu.md +0 -112
  299. package/docs/context.md +0 -81
  300. package/docs/css.md +0 -90
  301. package/docs/deep-link.md +0 -186
  302. package/docs/environment-detection.md +0 -112
  303. package/docs/fontawesome.md +0 -107
  304. package/docs/hooks.md +0 -89
  305. package/docs/icons.md +0 -79
  306. package/docs/index.md +0 -317
  307. package/docs/installer-options.md +0 -165
  308. package/docs/ipc.md +0 -61
  309. package/docs/lib-modules.md +0 -53
  310. package/docs/logging.md +0 -229
  311. package/docs/menu.md +0 -160
  312. package/docs/releasing.md +0 -239
  313. package/docs/remote-config.md +0 -118
  314. package/docs/remote-scripts.md +0 -144
  315. package/docs/restart-manager.md +0 -144
  316. package/docs/runner.md +0 -290
  317. package/docs/sentry.md +0 -97
  318. package/docs/shared/agent-docs.md +0 -89
  319. package/docs/shared/analytics.md +0 -612
  320. package/docs/shared/brands.md +0 -57
  321. package/docs/shared/breaking-changes.md +0 -851
  322. package/docs/shared/config.md +0 -1952
  323. package/docs/shared/deploys.md +0 -341
  324. package/docs/shared/icons.md +0 -219
  325. package/docs/shared/local-dev.md +0 -167
  326. package/docs/shared/logging.md +0 -205
  327. package/docs/shared/monitoring.md +0 -167
  328. package/docs/shared/publishing.md +0 -187
  329. package/docs/shared/rulings.md +0 -34
  330. package/docs/shared/testing.md +0 -147
  331. package/docs/shared/theming.md +0 -629
  332. package/docs/shared/translation.md +0 -333
  333. package/docs/shared/updates.md +0 -61
  334. package/docs/signing.md +0 -293
  335. package/docs/startup.md +0 -142
  336. package/docs/storage.md +0 -59
  337. package/docs/templating.md +0 -101
  338. package/docs/test-boot-layer.md +0 -157
  339. package/docs/test-framework.md +0 -362
  340. package/docs/themes.md +0 -149
  341. package/docs/tooltips.md +0 -99
  342. package/docs/tray.md +0 -164
  343. package/docs/usage.md +0 -58
  344. package/docs/verts.md +0 -62
  345. package/docs/windows.md +0 -149
@@ -5,17 +5,17 @@
5
5
  * That is correct for a STANDALONE target (the target dir is the git root) and dead on
6
6
  * arrival inside a brand monorepo: GitHub executes workflows from the REPO
7
7
  * ROOT's `.github/workflows/` only, so `targets/extension/.github/workflows/publish.yml`
8
- * never runs — CI builds and store publishes silently do not exist.
8
+ * never runs: CI builds and store publishes silently do not exist.
9
9
  *
10
10
  * In a monorepo the target's workflow is COMPOSED into the root dir instead: one
11
11
  * file per target (`<target>-<workflow>.yml`), every post-checkout `run:` step scoped
12
- * to the target's path, regenerated from the framework template on every setup — so
12
+ * to the target's path, regenerated from the framework template on every setup, so
13
13
  * a re-run updates the target's own file and can never duplicate a job. Each target's
14
14
  * runs also get their own concurrency group, so one target's deploy never cancels
15
15
  * another's.
16
16
  *
17
17
  * Scoping is by working directory, not by a `paths:` trigger filter: OMEGA
18
- * workflows carry NO push triggers by design (deliberate deploys — D13), and a
18
+ * workflows carry NO push triggers by design (deliberate deploys, D13), and a
19
19
  * path filter on a dispatch-only workflow filters nothing. It is declared PER
20
20
  * STEP, not as a workflow-level `defaults.run.working-directory`: that would
21
21
  * also scope the steps that run before actions/checkout (and the jobs that never
@@ -23,7 +23,7 @@
23
23
  *
24
24
  * Composition RECONCILES (#636): the enabled target set derives the composed
25
25
  * file set, so a target the brand's config no longer enables has its composed
26
- * files DELETED (reconcileComposedWorkflows) — the ratified example of a
26
+ * files DELETED (reconcileComposedWorkflows): the ratified example of a
27
27
  * dropped config key becoming a real removal.
28
28
  */
29
29
  const path = require('path');
@@ -68,7 +68,7 @@ const INSTALL_WORKSPACE_FLAG = '--workspace .';
68
68
  * an unscoped artifact upload collects an empty repo-root path.
69
69
  *
70
70
  * Named per action on purpose: a blanket "any input called path" rule would
71
- * rewrite inputs that mean something else — `actions/checkout`'s `path:` names
71
+ * rewrite inputs that mean something else: `actions/checkout`'s `path:` names
72
72
  * where to CLONE the repo, not a file in it. An action missing from this table
73
73
  * is left alone; add it here when a template starts using it.
74
74
  */
@@ -164,7 +164,7 @@ function composeWorkflow(contents, options) {
164
164
  // (and its own engines.node pin) sits beside it.
165
165
  composed = renderInstallWorkspace(composed, { composed: true });
166
166
 
167
- // Display name carries the target — two targets' runs are told apart in the
167
+ // Display name carries the target: two targets' runs are told apart in the
168
168
  // Actions list, where only the workflow name shows.
169
169
  composed = composed.replace(/^name:[ \t]*(.*)$/m, (full, value) => `name: ${value.trim()} (${targetPath})`);
170
170
 
@@ -173,7 +173,7 @@ function composeWorkflow(contents, options) {
173
173
  composed = composed.replace(/^(concurrency:\n(?:[ \t]+.*\n)*?[ \t]+group:[ \t]*)(.*)$/m, (full, prefix, value) => `${prefix}${targetName}-${value.trim()}`);
174
174
 
175
175
  // Every `run:` step that follows its job's checkout executes in the target dir
176
- // (`uses:` actions — checkout and friends — stay at the repo root, which is
176
+ // (`uses:` actions, checkout and friends, stay at the repo root, which is
177
177
  // what they want).
178
178
  composed = scopeRunSteps(composed, targetPath);
179
179
 
@@ -238,12 +238,12 @@ function composeTargetWorkflows(options) {
238
238
  /**
239
239
  * Delete the composed workflows of targets this brand no longer has (#636).
240
240
  * Composing is per-target and a dropped target's framework never runs again, so
241
- * the file it wrote at the brand root would outlive it forever — a workflow
241
+ * the file it wrote at the brand root would outlive it forever: a workflow
242
242
  * Actions still lists and still dispatches for a target that is gone.
243
243
  *
244
244
  * The brand root's `.github/workflows/` is a MIXED dir: the brand's own
245
245
  * human-authored workflows live beside the composed ones. So a file goes only
246
- * past a DOUBLE lock — it carries the GENERATED header (which also NAMES the
246
+ * past a DOUBLE lock: it carries the GENERATED header (which also NAMES the
247
247
  * target it was composed for) AND it is named the composed way,
248
248
  * `<target>-<framework file>.yml`. A human file matches neither and is never
249
249
  * even a candidate; nothing here reads or edits one.
@@ -283,14 +283,14 @@ function reconcileComposedWorkflows(options) {
283
283
  jetpack.remove(file);
284
284
  }
285
285
  result.removed.push(`.github/workflows/${name}`);
286
- logger.log(`Removed .github/workflows/${name} — ${owned.targetPath} is no longer a target of this brand`);
286
+ logger.log(`Removed .github/workflows/${name}: ${owned.targetPath} is no longer a target of this brand`);
287
287
  }
288
288
 
289
289
  return result;
290
290
  }
291
291
 
292
292
  /**
293
- * The workflow file name to dispatch for a target — composed in a brand monorepo,
293
+ * The workflow file name to dispatch for a target: composed in a brand monorepo,
294
294
  * the framework's own name standalone. The ONE place deploy verbs and the
295
295
  * compose step agree on the name.
296
296
  * @param {object} options
@@ -324,7 +324,7 @@ function composedWorkflowNameFor(name, workflow) {
324
324
  // A workflow-level `defaults.run.working-directory` reads cleaner but applies to
325
325
  // EVERY run step, including the ones that execute before actions/checkout (the
326
326
  // git config step, a matrix-resolving step) and the jobs that never check out at
327
- // all — the target dir does not exist there yet, and the job dies on step 1.
327
+ // all: the target dir does not exist there yet, and the job dies on step 1.
328
328
  function scopeRunSteps(contents, targetPath) {
329
329
  const lines = contents.split('\n');
330
330
  const output = [];
@@ -343,7 +343,7 @@ function scopeRunSteps(contents, targetPath) {
343
343
 
344
344
  for (const line of lines) {
345
345
  const indent = line.search(/\S/);
346
- // Comments and blank lines never close a block — they belong to what follows
346
+ // Comments and blank lines never close a block; they belong to what follows
347
347
  const structural = indent >= 0 && !line.trimStart().startsWith('#');
348
348
 
349
349
  // A top-level key closes any open steps list and says whether we are in `jobs:`
@@ -417,8 +417,8 @@ function actionOf(step, keyIndent) {
417
417
  return line ? line.slice(line.indexOf('uses:') + 5).trim().split('@')[0] : null;
418
418
  }
419
419
 
420
- // `hashFiles()` globs from GITHUB_WORKSPACE wherever it appears — no step key
421
- // moves it — so a composed target workflow's patterns carry the target path or match
420
+ // `hashFiles()` globs from GITHUB_WORKSPACE wherever it appears (no step key
421
+ // moves it), so a composed target workflow's patterns carry the target path or match
422
422
  // nothing at all.
423
423
  function scopeHashFiles(step, targetPath) {
424
424
  return step.map((line) => line.replace(/hashFiles\(([^)]*)\)/g, (full, args) => {
@@ -491,7 +491,7 @@ function scopeValue(value, targetPath) {
491
491
  }
492
492
 
493
493
  // A target-relative path/glob. Absolute paths and expression-built values name
494
- // something other than a file in the checkout — those are left as written.
494
+ // something other than a file in the checkout; those are left as written.
495
495
  function joinTargetPath(value, targetPath) {
496
496
  const trimmed = value.trim();
497
497
  const scoped = trimmed.replace(/^\.\//, '');
@@ -503,7 +503,7 @@ function joinTargetPath(value, targetPath) {
503
503
  return `${targetPath}/${scoped}`;
504
504
  }
505
505
 
506
- // Declare `working-directory` on one run step's lines — a step that already
506
+ // Declare `working-directory` on one run step's lines. A step that already
507
507
  // declares its own is left alone.
508
508
  function scopeRunStep(step, keyIndent, targetPath) {
509
509
  const pad = ' '.repeat(keyIndent);
@@ -544,11 +544,11 @@ function sweepTargetCopy(context) {
544
544
  jetpack.remove(targetCopy);
545
545
  pruneEmptyDirs(path.dirname(targetCopy), targetDir);
546
546
  result.removed.push(`.github/workflows/${name}`);
547
- logger.log(`Removed ${targetPath}/.github/workflows/${name} — GitHub only runs workflows from the repo root`);
547
+ logger.log(`Removed ${targetPath}/.github/workflows/${name}: GitHub only runs workflows from the repo root`);
548
548
  return;
549
549
  }
550
550
 
551
- logger.warn(`Kept ${targetPath}/.github/workflows/${name} — it differs from the current framework template (your edits, or an older framework version), and GitHub NEVER runs a workflow from a target dir. Compare it against ${composedName}, move anything it still needs, then delete ${targetPath}/.github/workflows/${name}`);
551
+ logger.warn(`Kept ${targetPath}/.github/workflows/${name}: it differs from the current framework template (your edits, or an older framework version), and GitHub NEVER runs a workflow from a target dir. Compare it against ${composedName}, move anything it still needs, then delete ${targetPath}/.github/workflows/${name}`);
552
552
  }
553
553
 
554
554
  // Is every line of the target's copy a line the current template still ships, in
@@ -559,7 +559,7 @@ function sweepTargetCopy(context) {
559
559
  // same file minus those lines, #334). A line the consumer ADDED or CHANGED is a
560
560
  // line no template of this framework ever shipped, so it fails here and the
561
561
  // copy is kept. Accepted blind spot: an edit that ONLY deletes lines is still a
562
- // subsequence and gets swept — bounded, because a target-dir workflow never runs
562
+ // subsequence and gets swept. Bounded, because a target-dir workflow never runs
563
563
  // and the composed root file is regenerated from the current template.
564
564
  function isFrameworkGeneration(existing, rendered) {
565
565
  const template = rendered.split('\n');
@@ -587,7 +587,7 @@ function pruneEmptyDirs(dir, rootDir) {
587
587
  }
588
588
  }
589
589
 
590
- // posix-style target path — this string ends up inside a YAML workflow.
590
+ // posix-style target path: this string ends up inside a YAML workflow.
591
591
  function relativePath(from, to) {
592
592
  return path.relative(from, to).split(path.sep).join('/');
593
593
  }
@@ -596,14 +596,14 @@ function relativePath(from, to) {
596
596
  // the file is framework-owned AND which target it belongs to, which is what
597
597
  // makes the reconcile able to tell a composed file from a brand's own workflow
598
598
  // without guessing from the name alone.
599
- const generatedMark = (targetPath) => `# GENERATED by the ${targetPath} framework scaffold — do not edit.`;
600
- // Reading accepts the LEGACY wording too (`# GENERATED by \`omega setup\` for
601
- // <targetPath> — do not edit.`): a target dropped before the rewording keeps its
602
- // old-header file forever, which is exactly the file the reconcile exists to
603
- // remove. Composing only ever writes the current sentence.
604
- const GENERATED_MARK = /^# GENERATED by (?:the (\S+) framework scaffold|`omega setup` for (\S+)) — do not edit\.$/;
605
-
606
- // Which target a brand-root workflow file was composed for — null when the file
599
+ const generatedMark = (targetPath) => `# GENERATED by the ${targetPath} framework scaffold. Do not edit.`;
600
+ // Reading also accepts the older forms (the `omega setup` wording, the em dash
601
+ // separator): a dropped target keeps its old-header file forever, which is exactly
602
+ // the file the reconcile exists to remove. Composing only ever writes the current
603
+ // sentence.
604
+ const GENERATED_MARK = /^# GENERATED by (?:the (\S+) framework scaffold|`omega setup` for (\S+))(?:\. Do| \u2014 do) not edit\.$/;
605
+
606
+ // Which target a brand-root workflow file was composed for, null when the file
607
607
  // is not a composed one. BOTH locks must hold: the generation mark on line 1,
608
608
  // and the `<target>-<framework file>.yml` naming the compose writes.
609
609
  function composedWorkflowOwner(name, contents) {
@@ -633,4 +633,4 @@ function header(targetPath) {
633
633
  ].join('\n');
634
634
  }
635
635
 
636
- module.exports = { composeWorkflow, composeTargetWorkflows, composedWorkflowName, composedWorkflowNameFor, reconcileComposedWorkflows, renderInstallFirewall, renderInstallWorkspace, FIREWALL_ACTION, FIREWALL_STEP_ID, FIREWALL_TOKEN, INSTALL_WORKSPACE_TOKEN, INSTALL_WORKSPACE_FLAG };
636
+ module.exports = { composeWorkflow, composeTargetWorkflows, composedWorkflowName, composedWorkflowNameFor, composedWorkflowOwner, reconcileComposedWorkflows, renderInstallFirewall, renderInstallWorkspace, FIREWALL_ACTION, FIREWALL_STEP_ID, FIREWALL_TOKEN, INSTALL_WORKSPACE_TOKEN, INSTALL_WORKSPACE_FLAG };
@@ -2,19 +2,18 @@
2
2
  //
3
3
  // Every framework bin parses argv (src/argv.js) and hands the result to a Main class
4
4
  // whose process() resolves a command name and runs commands/<name>.js from the
5
- // framework's dist. That router was copy-pasted per framework (UJM/BXM/EM,
6
- // drift-identical); this is the single implementation. @omega.js/backend's CLI is a different
5
+ // framework's dist; this is the single implementation. @omega.js/backend's CLI is a different
7
6
  // design (colon-style utility commands dispatching stateful command classes)
8
7
  // and does not use this.
9
8
  //
10
- // Resolution order (unchanged from the framework copies):
9
+ // Resolution order:
11
10
  // 1. Positional command (`mgr build`) — matched against command names and
12
11
  // their aliases; unknown names pass through as-is (missing file = error).
13
12
  // 2. Flag-style alias (`mgr --build`, `mgr -b`) — first alias-table hit wins.
14
13
  // 3. The default command (OMEGA convention: help — `setup` is retired,
15
14
  // #675, and every verb runs its own local scaffold).
16
15
  //
17
- // dotenv loading and any process-env fixups (e.g. EM's ELECTRON_RUN_AS_NODE
16
+ // dotenv loading and any process-env fixups (e.g. desktop's ELECTRON_RUN_AS_NODE
18
17
  // strip) stay in the framework's cli.js/bin — they're framework concerns, not
19
18
  // dispatch.
20
19
 
@@ -28,6 +27,7 @@ const jetpack = require('fs-jetpack');
28
27
  * @param {string} config.commandsDir - Absolute directory of command modules — <name>.js exporting `async (options) => {}`
29
28
  * @param {Object<string, string[]>} [config.aliases] - Command name → positional/flag aliases (e.g. `{ install: ['-i', 'i', '--install'] }`)
30
29
  * @param {string} [config.defaultCommand='help'] - Command used when no positional or flag alias matches
30
+ * @param {Function} [config.fallback] - `(command) => handler|null`: the handler for a command with no file, else null for the unknown answer
31
31
  * @returns {Function} Main class — bins do `new Main(argv)` then `await main.process(argv)`
32
32
  */
33
33
  function createCliRouter(config) {
@@ -36,6 +36,7 @@ function createCliRouter(config) {
36
36
  const commandsDir = config.commandsDir;
37
37
  const aliases = config.aliases || {};
38
38
  const defaultCommand = config.defaultCommand || 'help';
39
+ const fallback = config.fallback || null;
39
40
 
40
41
  if (!commandsDir) {
41
42
  throw new Error('[devkit cli-router] commandsDir is required');
@@ -115,14 +116,18 @@ function createCliRouter(config) {
115
116
 
116
117
  // Get the command file path
117
118
  const commandFile = path.join(commandsDir, `${command}.js`);
119
+ const exists = jetpack.exists(commandFile);
118
120
 
119
- if (!jetpack.exists(commandFile)) {
120
- // Built-in help (a framework's own commands/help.js would have won above)
121
- if (command === 'help') {
122
- printHelp();
123
- return;
124
- }
121
+ // Built-in help (a framework's own commands/help.js would have won above)
122
+ if (!exists && command === 'help') {
123
+ printHelp();
124
+ return;
125
+ }
126
+
127
+ // A router can own verbs it keeps no file for; a null handler is the unknown answer
128
+ const handler = !exists && fallback ? fallback(command) : null;
125
129
 
130
+ if (!exists && !handler) {
126
131
  // Unknown command: name the valid surface and fail loud — the old path
127
132
  // threw a doubled "Error executing… Error: Command…" with no listing.
128
133
  // An empty listing means the dist itself is broken, not a typo — say so.
@@ -138,7 +143,7 @@ function createCliRouter(config) {
138
143
 
139
144
  try {
140
145
  // Execute the command
141
- const Command = require(commandFile);
146
+ const Command = handler || require(commandFile);
142
147
  await Command(options);
143
148
  } catch (e) {
144
149
  // The command's own error is the only surface — no wrapper prefix, no
@@ -1,42 +1,8 @@
1
- // applyDefaults(config) — the shared OMEGA defaults-scaffolding engine.
2
- //
3
- // Every framework ships a defaults tree (src/defaults/ → dist/defaults/) that
4
- // setup copies into the consumer project. The copy rules lived as two divergent
5
- // implementations (BXM's gulp FILE_MAP task — the real one — and EM's plain-fs
6
- // copyDefaults; UJM carries a third copy of the BXM shape). This is the single
7
- // plain-fs engine, a normalized superset of both.
8
- //
9
- // File-map rules: minimatch patterns (dot:true) matched against the RAW path
10
- // relative to defaultsDir, last-match-wins option merging. Per-rule options:
11
- // overwrite bool|fn(item) default true — write even if the destination exists
12
- // skip bool|fn(item) default false — never process this file
13
- // name fn(item) rename the output file
14
- // path fn(item) re-destination the output (relative dir)
15
- // template object render `{{ key.path }}` tokens with this data
16
- // (tolerant: unknown keys survive verbatim, so GitHub
17
- // Actions' `${{ secrets.X }}` passes through)
18
- // merge bool JSON5 defaults-merge with the existing file, written
19
- // through @omega.js/config's comment-preserving editor
20
- // (only the differing keys change; comments stay)
21
- // mergeLines bool OMEGA marker-section line merge (.env/.gitignore/AGENTS.md)
22
- // retire bool brand-context per-target doc retirement: NEVER scaffold
23
- // the file; an existing framework-owned-only copy is
24
- // DELETED (one-time heal — the brand root is the doc
25
- // home), a copy carrying consumer content stays with a
26
- // loud move-it warning (never destroyed)
27
- //
28
- // Engine built-ins (not expressed in the file map):
29
- // - `_.name` segments lose the leading `_` (dotfiles ship past npm's filter)
30
- // - non-final segments starting `_` (but not `_.`) are ARCHIVE dirs — skipped
31
- // (reference material that ships in the framework package, e.g. EM's `_mas/`)
32
- // - `.gitkeep` creates the destination directory, the file itself never copies
33
- // - `.DS_Store` never copies
34
- // - text writes are skipped when the destination is byte-identical (idempotent
35
- // re-runs report zero writes)
36
- // - a destination carrying the OMEGA section markers when the current template
37
- // no longer does is a PRIOR-GENERATION generated file: framework-owned, so
38
- // `retire` sweeps it and `overwrite: false` still heals it
39
- // - binary files (by extension) copy verbatim — never templated/merged/transformed
1
+ // applyDefaults(config): the ONE plain-fs defaults-scaffolding engine every
2
+ // framework's scaffold runs, copying its defaults tree into the consumer
3
+ // project. The file-map options (overwrite, skip, name, path, template, merge,
4
+ // mergeLines, retire), the built-ins and the minimatch matching rules live in
5
+ // docs/devkit/index.md.
40
6
 
41
7
  const path = require('path');
42
8
  const fs = require('fs');
@@ -46,7 +12,7 @@ const { minimatch } = require('minimatch');
46
12
  const { mergeLineBasedFiles, hasSectionMarkers, getCustomSection } = require('./merge-line-files');
47
13
 
48
14
  // Files with these extensions copy byte-for-byte and never go through
49
- // template/merge/transform (matches BXM's binary detection list).
15
+ // template/merge/transform.
50
16
  const BINARY_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|otf|eot|pdf|zip|tar|gz|mp3|mp4|avi|mov)$/i;
51
17
 
52
18
  const RULE_DEFAULTS = {
@@ -70,7 +36,7 @@ const RULE_DEFAULTS = {
70
36
  * @param {Object<string, object>} [config.fileMap] - minimatch pattern → rule options (see header)
71
37
  * @param {string[]} [config.files] - Only process these absolute source paths (watch single-file mode)
72
38
  * @param {Function} [config.transform] - `(contents, item) => contents` global hook, run on every
73
- * non-binary file after per-rule processing (BXM's site-token templating pass)
39
+ * non-binary file after per-rule processing (e.g. extension's site-token templating pass)
74
40
  * @param {object} [config.logger] - `{ log, warn, error }` (defaults to console)
75
41
  * @returns {{ written: string[], merged: string[], skipped: string[], removed: string[] }} destination-relative paths per outcome
76
42
  */
@@ -202,7 +168,7 @@ function applyDefaults(config) {
202
168
  let didMerge = false;
203
169
 
204
170
  // Per-rule template render happens BEFORE merging, so merges compare
205
- // rendered framework defaults against the consumer's file (EM semantic).
171
+ // rendered framework defaults against the consumer's file.
206
172
  if (options.template) {
207
173
  contents = renderTemplate(contents, options.template);
208
174
  }
@@ -266,12 +232,10 @@ function isFrameworkOwned(existing, rendered) {
266
232
  return existing.trim() === rendered.trim();
267
233
  }
268
234
 
269
- // A generation change: the destination carries the OMEGA marker grammar — which
270
- // only this framework's own scaffold writes — while the current template has
271
- // dropped it. That file is a PRIOR GENERATION's generated copy (e.g. the
272
- // content-bearing CLAUDE.md written before it became the one-line `@AGENTS.md`
273
- // pointer), so comparing it against the current render can never match and the
274
- // framework, not the consumer, owns it.
235
+ // A generation change: the destination carries the OMEGA marker grammar (only
236
+ // this framework's scaffold writes it) while the current template dropped it.
237
+ // That file is a PRIOR GENERATION's copy: it can never match the current
238
+ // render, and the framework, not the consumer, owns it.
275
239
  function isLegacyMarkerArtifact(existing, rendered) {
276
240
  return hasSectionMarkers(existing) && !hasSectionMarkers(rendered);
277
241
  }
@@ -309,7 +273,7 @@ function strippedSegments(segments) {
309
273
  return segments.map((s) => (s.startsWith('_.') ? s.slice(1) : s));
310
274
  }
311
275
 
312
- // Last-match-wins option resolution across the file map (BXM's getFileOptions).
276
+ // Last-match-wins option resolution across the file map.
313
277
  function resolveOptions(relativePath, fileMap) {
314
278
  let options = { ...RULE_DEFAULTS };
315
279
 
@@ -323,7 +287,7 @@ function resolveOptions(relativePath, fileMap) {
323
287
  return options;
324
288
  }
325
289
 
326
- // Minimal `{{ key.path }}` substitution (EM's tolerant renderer — the standard).
290
+ // Minimal `{{ key.path }}` substitution, tolerant of unknown keys.
327
291
  // Unknown keys render as the original `{{ ... }}` string, so non-template content
328
292
  // with literal braces (e.g. GitHub Actions `${{ secrets.X }}`) survives.
329
293
  function renderTemplate(content, context) {
@@ -334,7 +298,7 @@ function renderTemplate(content, context) {
334
298
  });
335
299
  }
336
300
 
337
- // JSON5 defaults merge (BXM's mergeConfigs): framework defaults provide shape and
301
+ // JSON5 defaults merge: framework defaults provide shape and
338
302
  // order; the consumer's values win unless they're the literal string 'default';
339
303
  // consumer-only keys survive at every nesting level, EXCEPT one holding the
340
304
  // 'default' sentinel, which is the framework's own unset marker and drops (#926).
@@ -59,6 +59,7 @@ const { setTimeout: delay } = require('node:timers/promises');
59
59
  const jetpack = require('fs-jetpack');
60
60
  const { resolveCompany, COMPANY_RESOLVED_FILE } = require('../config/index.js');
61
61
  const { gitAuthEnv, scrubToken } = require('./git-auth.js');
62
+ const { composedWorkflowOwner } = require('./ci-workflows.js');
62
63
 
63
64
  // Constants
64
65
  const API_BASE = 'https://api.github.com';
@@ -342,7 +343,9 @@ async function defaultBranchOf({ owner, repo, token, fetchFn = fetch }) {
342
343
  * this write exists, and it is the only write a deploy ever makes to a default
343
344
  * branch (Ian, 2026-09-14: "if all we are pushing to main is the gh workflow
344
345
  * files that's fine"). Each file's bytes are compared with what the branch
345
- * holds; when every one matches, nothing is written at all.
346
+ * holds; when every one matches, nothing is written at all. A branch file
347
+ * that carries the GENERATED header and is no longer composed (a renamed or
348
+ * dropped target) is RETIRED in the same commit; a hand-written one never is.
346
349
  *
347
350
  * Through the git DATA api, never a working-tree commit: blobs, a tree on top
348
351
  * of the branch's own, a commit whose parent is its head. An EMPTY repo (no
@@ -356,14 +359,14 @@ async function defaultBranchOf({ owner, repo, token, fetchFn = fetch }) {
356
359
  * @param {string} options.token - The GitHub token.
357
360
  * @param {Function} [options.fetchFn] - Injectable fetch (tests).
358
361
  * @param {object} [options.logger] - Logger with `log` (silent when omitted).
359
- * @returns {Promise<{ pushed: string[], sha: string|null }>} What was written.
362
+ * @returns {Promise<{ pushed: string[], removed: string[], sha: string|null }>} What was written.
360
363
  */
361
364
  async function pushWorkflowFiles({ brandRoot, owner, repo, branch, token, fetchFn = fetch, logger }) {
362
365
  const files = composedWorkflowFiles(brandRoot);
363
366
 
364
367
  if (files.length === 0) {
365
368
  if (logger) logger.log(`No composed workflow in ${brandRoot}/${WORKFLOWS_DIR}: ${branch} is untouched.`);
366
- return { pushed: [], sha: null };
369
+ return { pushed: [], removed: [], sha: null };
367
370
  }
368
371
 
369
372
  const call = async (route, init) => {
@@ -399,9 +402,31 @@ async function pushWorkflowFiles({ brandRoot, owner, repo, branch, token, fetchF
399
402
  }
400
403
  }
401
404
 
402
- if (changed.length === 0) {
405
+ // What the branch holds that this brand no longer composes. The same double
406
+ // lock as the local prune: the GENERATED header and the composed naming, so
407
+ // a hand-written workflow is never a candidate. No listing means no files.
408
+ const composed = new Set(files.map((file) => file.name));
409
+ const listing = await read(
410
+ await call(`/contents/${WORKFLOWS_DIR}?ref=${encodeURIComponent(branch)}`),
411
+ [200, 404],
412
+ `Listing ${WORKFLOWS_DIR}`,
413
+ );
414
+ const retired = [];
415
+
416
+ for (const entry of listing || []) {
417
+ if (entry.type !== 'file' || !entry.name.endsWith('.yml') || composed.has(entry.name)) {
418
+ continue;
419
+ }
420
+
421
+ const held = await read(await call(`/contents/${entry.path}?ref=${encodeURIComponent(branch)}`), [200], `Reading ${entry.name}`);
422
+ if (composedWorkflowOwner(entry.name, Buffer.from(held.content || '', 'base64').toString('utf8'))) {
423
+ retired.push({ name: entry.name, path: entry.path });
424
+ }
425
+ }
426
+
427
+ if (changed.length === 0 && retired.length === 0) {
403
428
  if (logger) logger.log(`${owner}/${repo}#${branch} already carries ${files.map((file) => file.name).join(', ')}: nothing pushed to ${branch}.`);
404
- return { pushed: [], sha: null };
429
+ return { pushed: [], removed: [], sha: null };
405
430
  }
406
431
 
407
432
  // The head this commit sits on. A repo with no commit at all answers 404, and
@@ -422,15 +447,21 @@ async function pushWorkflowFiles({ brandRoot, owner, repo, branch, token, fetchF
422
447
  );
423
448
  tree.push({ path: file.path, mode: '100644', type: 'blob', sha: blob.sha });
424
449
  }
450
+ for (const file of retired) {
451
+ tree.push({ path: file.path, mode: '100644', type: 'blob', sha: null });
452
+ }
425
453
 
426
- const names = changed.map((file) => file.name).join(', ');
454
+ const names = [
455
+ ...(changed.length ? [`compose ${changed.map((file) => file.name).join(', ')}`] : []),
456
+ ...(retired.length ? [`retire ${retired.map((file) => file.name).join(', ')}`] : []),
457
+ ].join(', ');
427
458
  const written = await read(
428
459
  await call('/git/trees', { method: 'POST', body: JSON.stringify({ ...(baseTree ? { base_tree: baseTree } : {}), tree }) }),
429
460
  [201],
430
461
  'Writing the tree',
431
462
  );
432
463
  const commit = await read(
433
- await call('/git/commits', { method: 'POST', body: JSON.stringify({ message: `chore(ci): compose ${names}`, tree: written.sha, parents }) }),
464
+ await call('/git/commits', { method: 'POST', body: JSON.stringify({ message: `chore(ci): ${names}`, tree: written.sha, parents }) }),
434
465
  [201],
435
466
  'Writing the commit',
436
467
  );
@@ -444,10 +475,14 @@ async function pushWorkflowFiles({ brandRoot, owner, repo, branch, token, fetchF
444
475
  );
445
476
 
446
477
  if (logger) {
447
- logger.log(`Pushed ${changed.map((file) => `${file.name} (${file.reason})`).join(', ')} to ${owner}/${repo}#${branch} as ${shortSha(commit.sha)}: GitHub registers a workflow from the default branch.`);
478
+ const touched = [
479
+ ...changed.map((file) => `${file.name} (${file.reason})`),
480
+ ...retired.map((file) => `${file.name} (retired)`),
481
+ ];
482
+ logger.log(`Pushed ${touched.join(', ')} to ${owner}/${repo}#${branch} as ${shortSha(commit.sha)}: GitHub registers a workflow from the default branch.`);
448
483
  }
449
484
 
450
- return { pushed: changed.map((file) => file.name), sha: commit.sha };
485
+ return { pushed: changed.map((file) => file.name), removed: retired.map((file) => file.name), sha: commit.sha };
451
486
  }
452
487
 
453
488
  /**