@omega.js/desktop 0.1.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 (818) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +166 -0
  3. package/bin/omega +2 -0
  4. package/bin/omega-desktop +2 -0
  5. package/bin/omg +2 -0
  6. package/dist/assets/css/core/_fontawesome.scss +146 -0
  7. package/dist/assets/css/core/_initialize.scss +38 -0
  8. package/dist/assets/css/omega-desktop.scss +39 -0
  9. package/dist/assets/css/shell/_index.scss +207 -0
  10. package/dist/assets/css/tokens/_index.scss +166 -0
  11. package/dist/assets/js/bootstrap.bundle.js +1 -0
  12. package/dist/assets/js/core/app-shell.js +136 -0
  13. package/dist/assets/themes/_template/README.md +115 -0
  14. package/dist/assets/themes/_template/_config.scss +60 -0
  15. package/dist/assets/themes/_template/_theme.js +14 -0
  16. package/dist/assets/themes/_template/_theme.scss +28 -0
  17. package/dist/assets/themes/_template/css/base/_root.scss +19 -0
  18. package/dist/assets/themes/_template/css/components/_components.scss +23 -0
  19. package/dist/assets/themes/base/README.md +5 -0
  20. package/dist/assets/themes/base/_components/data/org-chart/component.html +33 -0
  21. package/dist/assets/themes/base/_components/data/org-chart/component.json5 +33 -0
  22. package/dist/assets/themes/base/_components/data/org-chart/component.scss +183 -0
  23. package/dist/assets/themes/base/_components/heading/masthead/component.html +22 -0
  24. package/dist/assets/themes/base/_components/heading/masthead/component.json5 +30 -0
  25. package/dist/assets/themes/base/_components/heading/section-head/component.html +18 -0
  26. package/dist/assets/themes/base/_components/heading/section-head/component.json5 +27 -0
  27. package/dist/assets/themes/base/_components/pricing/features/component.html +29 -0
  28. package/dist/assets/themes/base/_components/pricing/features/component.json5 +30 -0
  29. package/dist/assets/themes/base/_hero/orbit/index.html +30 -0
  30. package/dist/assets/themes/base/_hero/orbit/script.js +33 -0
  31. package/dist/assets/themes/base/_hero/orbit/style.scss +133 -0
  32. package/dist/assets/themes/base/_includes/admin/sections/sidebar.html +3 -0
  33. package/dist/assets/themes/base/_includes/admin/sections/topbar.html +3 -0
  34. package/dist/assets/themes/base/_includes/backend/sections/sidebar.html +3 -0
  35. package/dist/assets/themes/base/_includes/backend/sections/topbar.html +3 -0
  36. package/dist/assets/themes/base/_includes/frontend/components/connection-card.html +49 -0
  37. package/dist/assets/themes/base/_includes/frontend/components/post-card.html +54 -0
  38. package/dist/assets/themes/base/_includes/frontend/sections/account-connections.html +42 -0
  39. package/dist/assets/themes/base/_includes/frontend/sections/account-section-header.html +11 -0
  40. package/dist/assets/themes/base/_includes/frontend/sections/footer.html +154 -0
  41. package/dist/assets/themes/base/_includes/frontend/sections/nav.html +276 -0
  42. package/dist/assets/themes/base/_includes/global/sections/account.html +72 -0
  43. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +248 -0
  44. package/dist/assets/themes/base/_includes/global/sections/app-topbar.html +228 -0
  45. package/dist/assets/themes/base/_includes/global/sections/page-header.html +99 -0
  46. package/dist/assets/themes/base/_layouts/admin/core/minimal-viewport-locked.html +22 -0
  47. package/dist/assets/themes/base/_layouts/admin/core/minimal.html +23 -0
  48. package/dist/assets/themes/base/_layouts/backend/core/base.html +57 -0
  49. package/dist/assets/themes/base/_layouts/backend/core/minimal-viewport-locked.html +32 -0
  50. package/dist/assets/themes/base/_layouts/backend/core/minimal.html +32 -0
  51. package/dist/assets/themes/base/_layouts/backend/pages/dashboard/index.html +230 -0
  52. package/dist/assets/themes/base/_layouts/frontend/core/base.html +31 -0
  53. package/dist/assets/themes/base/_layouts/frontend/core/cover.html +40 -0
  54. package/dist/assets/themes/base/_layouts/frontend/core/minimal.html +47 -0
  55. package/dist/assets/themes/base/_layouts/frontend/pages/404.html +54 -0
  56. package/dist/assets/themes/base/_layouts/frontend/pages/about.html +144 -0
  57. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +2104 -0
  58. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/alternative.html +319 -0
  59. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +186 -0
  60. package/dist/assets/themes/base/_layouts/frontend/pages/app.html +142 -0
  61. package/dist/assets/themes/base/_layouts/frontend/pages/auth/reset.html +53 -0
  62. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signin.html +90 -0
  63. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signup.html +141 -0
  64. package/dist/assets/themes/base/_layouts/frontend/pages/auth/token.html +44 -0
  65. package/dist/assets/themes/base/_layouts/frontend/pages/blog/categories/category.html +88 -0
  66. package/dist/assets/themes/base/_layouts/frontend/pages/blog/categories/index.html +88 -0
  67. package/dist/assets/themes/base/_layouts/frontend/pages/blog/index.html +255 -0
  68. package/dist/assets/themes/base/_layouts/frontend/pages/blog/post.html +261 -0
  69. package/dist/assets/themes/base/_layouts/frontend/pages/blog/tags/index.html +84 -0
  70. package/dist/assets/themes/base/_layouts/frontend/pages/blog/tags/tag.html +88 -0
  71. package/dist/assets/themes/base/_layouts/frontend/pages/collection/category.html +72 -0
  72. package/dist/assets/themes/base/_layouts/frontend/pages/collection/document.html +54 -0
  73. package/dist/assets/themes/base/_layouts/frontend/pages/collection/index.html +84 -0
  74. package/dist/assets/themes/base/_layouts/frontend/pages/connections/callback.html +55 -0
  75. package/dist/assets/themes/base/_layouts/frontend/pages/contact.html +214 -0
  76. package/dist/assets/themes/base/_layouts/frontend/pages/download.html +793 -0
  77. package/dist/assets/themes/base/_layouts/frontend/pages/extension/index.html +253 -0
  78. package/dist/assets/themes/base/_layouts/frontend/pages/extension/installed.html +198 -0
  79. package/dist/assets/themes/base/_layouts/frontend/pages/feedback.html +171 -0
  80. package/dist/assets/themes/base/_layouts/frontend/pages/index.html +66 -0
  81. package/dist/assets/themes/base/_layouts/frontend/pages/legal/document.html +78 -0
  82. package/dist/assets/themes/base/_layouts/frontend/pages/payment/checkout.html +459 -0
  83. package/dist/assets/themes/base/_layouts/frontend/pages/payment/confirmation.html +131 -0
  84. package/dist/assets/themes/base/_layouts/frontend/pages/portal/email-preferences.html +104 -0
  85. package/dist/assets/themes/base/_layouts/frontend/pages/pricing.html +596 -0
  86. package/dist/assets/themes/base/_layouts/frontend/pages/status.html +361 -0
  87. package/dist/assets/themes/base/_layouts/frontend/pages/team/index.html +210 -0
  88. package/dist/assets/themes/base/_layouts/frontend/pages/team/member.html +125 -0
  89. package/dist/assets/themes/base/_layouts/frontend/pages/updates/index.html +119 -0
  90. package/dist/assets/themes/base/_layouts/frontend/pages/updates/update.html +118 -0
  91. package/dist/assets/themes/base/_sections/about/hero/section.html +68 -0
  92. package/dist/assets/themes/base/_sections/about/hero/section.json5 +58 -0
  93. package/dist/assets/themes/base/_sections/about/letter/section.html +60 -0
  94. package/dist/assets/themes/base/_sections/about/letter/section.json5 +56 -0
  95. package/dist/assets/themes/base/_sections/about/photo-band/section.html +24 -0
  96. package/dist/assets/themes/base/_sections/about/photo-band/section.json5 +23 -0
  97. package/dist/assets/themes/base/_sections/about/principles/section.html +17 -0
  98. package/dist/assets/themes/base/_sections/about/principles/section.json5 +34 -0
  99. package/dist/assets/themes/base/_sections/about/timeline/section.html +20 -0
  100. package/dist/assets/themes/base/_sections/about/timeline/section.json5 +34 -0
  101. package/dist/assets/themes/base/_sections/marketing/bento/section.html +91 -0
  102. package/dist/assets/themes/base/_sections/marketing/bento/section.json5 +152 -0
  103. package/dist/assets/themes/base/_sections/marketing/cta/section.html +44 -0
  104. package/dist/assets/themes/base/_sections/marketing/cta/section.json5 +47 -0
  105. package/dist/assets/themes/base/_sections/marketing/faq/section.html +52 -0
  106. package/dist/assets/themes/base/_sections/marketing/faq/section.json5 +68 -0
  107. package/dist/assets/themes/base/_sections/marketing/hero/section.html +349 -0
  108. package/dist/assets/themes/base/_sections/marketing/hero/section.json5 +335 -0
  109. package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.html +38 -0
  110. package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.js +86 -0
  111. package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.json5 +29 -0
  112. package/dist/assets/themes/base/_sections/marketing/pricing-cards/section.html +115 -0
  113. package/dist/assets/themes/base/_sections/marketing/pricing-cards/section.json5 +72 -0
  114. package/dist/assets/themes/base/_sections/marketing/product-demo/section.html +62 -0
  115. package/dist/assets/themes/base/_sections/marketing/product-demo/section.js +44 -0
  116. package/dist/assets/themes/base/_sections/marketing/product-demo/section.json5 +52 -0
  117. package/dist/assets/themes/base/_sections/marketing/prose/section.html +22 -0
  118. package/dist/assets/themes/base/_sections/marketing/prose/section.json5 +41 -0
  119. package/dist/assets/themes/base/_sections/marketing/showcase/section.html +35 -0
  120. package/dist/assets/themes/base/_sections/marketing/showcase/section.json5 +52 -0
  121. package/dist/assets/themes/base/_sections/marketing/stats/section.html +32 -0
  122. package/dist/assets/themes/base/_sections/marketing/stats/section.json5 +44 -0
  123. package/dist/assets/themes/base/_sections/marketing/testimonials/section.html +54 -0
  124. package/dist/assets/themes/base/_sections/marketing/testimonials/section.json5 +70 -0
  125. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.html +30 -0
  126. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.json5 +38 -0
  127. package/dist/assets/themes/base/_sections/verts/unit/section.html +7 -0
  128. package/dist/assets/themes/base/_sections/verts/unit/section.js +22 -0
  129. package/dist/assets/themes/base/_sections/verts/unit/section.json5 +24 -0
  130. package/dist/assets/themes/base/_sections/verts/unit/section.scss +24 -0
  131. package/dist/assets/themes/base/_theme.js +13 -0
  132. package/dist/assets/themes/base/_theme.scss +4 -0
  133. package/dist/assets/themes/bootstrap/_theme.js +16 -0
  134. package/dist/assets/themes/bootstrap/_theme.scss +11 -0
  135. package/dist/assets/themes/bootstrap/js/index.esm.js +19 -0
  136. package/dist/assets/themes/bootstrap/js/index.umd.js +34 -0
  137. package/dist/assets/themes/bootstrap/js/src/alert.js +87 -0
  138. package/dist/assets/themes/bootstrap/js/src/base-component.js +86 -0
  139. package/dist/assets/themes/bootstrap/js/src/button.js +72 -0
  140. package/dist/assets/themes/bootstrap/js/src/carousel.js +474 -0
  141. package/dist/assets/themes/bootstrap/js/src/collapse.js +297 -0
  142. package/dist/assets/themes/bootstrap/js/src/dom/data.js +55 -0
  143. package/dist/assets/themes/bootstrap/js/src/dom/event-handler.js +317 -0
  144. package/dist/assets/themes/bootstrap/js/src/dom/manipulator.js +71 -0
  145. package/dist/assets/themes/bootstrap/js/src/dom/selector-engine.js +126 -0
  146. package/dist/assets/themes/bootstrap/js/src/dropdown.js +458 -0
  147. package/dist/assets/themes/bootstrap/js/src/modal.js +378 -0
  148. package/dist/assets/themes/bootstrap/js/src/offcanvas.js +282 -0
  149. package/dist/assets/themes/bootstrap/js/src/popover.js +97 -0
  150. package/dist/assets/themes/bootstrap/js/src/scrollspy.js +296 -0
  151. package/dist/assets/themes/bootstrap/js/src/tab.js +315 -0
  152. package/dist/assets/themes/bootstrap/js/src/toast.js +224 -0
  153. package/dist/assets/themes/bootstrap/js/src/tooltip.js +632 -0
  154. package/dist/assets/themes/bootstrap/js/src/util/backdrop.js +151 -0
  155. package/dist/assets/themes/bootstrap/js/src/util/component-functions.js +35 -0
  156. package/dist/assets/themes/bootstrap/js/src/util/config.js +65 -0
  157. package/dist/assets/themes/bootstrap/js/src/util/focustrap.js +115 -0
  158. package/dist/assets/themes/bootstrap/js/src/util/index.js +306 -0
  159. package/dist/assets/themes/bootstrap/js/src/util/sanitizer.js +117 -0
  160. package/dist/assets/themes/bootstrap/js/src/util/scrollbar.js +114 -0
  161. package/dist/assets/themes/bootstrap/js/src/util/swipe.js +146 -0
  162. package/dist/assets/themes/bootstrap/js/src/util/template-factory.js +160 -0
  163. package/dist/assets/themes/bootstrap/overrides/_accordion.scss +21 -0
  164. package/dist/assets/themes/bootstrap/overrides/_avatars.scss +51 -0
  165. package/dist/assets/themes/bootstrap/overrides/_blog-images.scss +45 -0
  166. package/dist/assets/themes/bootstrap/overrides/_buttons-adaptive.scss +141 -0
  167. package/dist/assets/themes/bootstrap/overrides/_color-shades.scss +25 -0
  168. package/dist/assets/themes/bootstrap/overrides/_cursor-utilities.scss +13 -0
  169. package/dist/assets/themes/bootstrap/overrides/_index.scss +30 -0
  170. package/dist/assets/themes/bootstrap/overrides/_links.scss +26 -0
  171. package/dist/assets/themes/bootstrap/overrides/_soft-colors.scss +99 -0
  172. package/dist/assets/themes/bootstrap/overrides/_spacing-extended.scss +74 -0
  173. package/dist/assets/themes/bootstrap/overrides/_spinners.scss +22 -0
  174. package/dist/assets/themes/bootstrap/overrides/_typography-utilities.scss +40 -0
  175. package/dist/assets/themes/bootstrap/scss/_accordion.scss +153 -0
  176. package/dist/assets/themes/bootstrap/scss/_alert.scss +68 -0
  177. package/dist/assets/themes/bootstrap/scss/_badge.scss +38 -0
  178. package/dist/assets/themes/bootstrap/scss/_breadcrumb.scss +40 -0
  179. package/dist/assets/themes/bootstrap/scss/_button-group.scss +147 -0
  180. package/dist/assets/themes/bootstrap/scss/_buttons.scss +216 -0
  181. package/dist/assets/themes/bootstrap/scss/_card.scss +238 -0
  182. package/dist/assets/themes/bootstrap/scss/_carousel.scss +226 -0
  183. package/dist/assets/themes/bootstrap/scss/_close.scss +66 -0
  184. package/dist/assets/themes/bootstrap/scss/_containers.scss +41 -0
  185. package/dist/assets/themes/bootstrap/scss/_dropdown.scss +250 -0
  186. package/dist/assets/themes/bootstrap/scss/_forms.scss +9 -0
  187. package/dist/assets/themes/bootstrap/scss/_functions.scss +302 -0
  188. package/dist/assets/themes/bootstrap/scss/_grid.scss +39 -0
  189. package/dist/assets/themes/bootstrap/scss/_helpers.scss +12 -0
  190. package/dist/assets/themes/bootstrap/scss/_images.scss +42 -0
  191. package/dist/assets/themes/bootstrap/scss/_list-group.scss +199 -0
  192. package/dist/assets/themes/bootstrap/scss/_maps.scss +174 -0
  193. package/dist/assets/themes/bootstrap/scss/_mixins.scss +42 -0
  194. package/dist/assets/themes/bootstrap/scss/_modal.scss +240 -0
  195. package/dist/assets/themes/bootstrap/scss/_nav.scss +197 -0
  196. package/dist/assets/themes/bootstrap/scss/_navbar.scss +289 -0
  197. package/dist/assets/themes/bootstrap/scss/_offcanvas.scss +147 -0
  198. package/dist/assets/themes/bootstrap/scss/_pagination.scss +109 -0
  199. package/dist/assets/themes/bootstrap/scss/_placeholders.scss +51 -0
  200. package/dist/assets/themes/bootstrap/scss/_popover.scss +196 -0
  201. package/dist/assets/themes/bootstrap/scss/_progress.scss +68 -0
  202. package/dist/assets/themes/bootstrap/scss/_reboot.scss +611 -0
  203. package/dist/assets/themes/bootstrap/scss/_root.scss +187 -0
  204. package/dist/assets/themes/bootstrap/scss/_spinners.scss +85 -0
  205. package/dist/assets/themes/bootstrap/scss/_tables.scss +171 -0
  206. package/dist/assets/themes/bootstrap/scss/_toasts.scss +73 -0
  207. package/dist/assets/themes/bootstrap/scss/_tooltip.scss +119 -0
  208. package/dist/assets/themes/bootstrap/scss/_transitions.scss +27 -0
  209. package/dist/assets/themes/bootstrap/scss/_type.scss +106 -0
  210. package/dist/assets/themes/bootstrap/scss/_utilities.scss +806 -0
  211. package/dist/assets/themes/bootstrap/scss/_variables-dark.scss +102 -0
  212. package/dist/assets/themes/bootstrap/scss/_variables.scss +1753 -0
  213. package/dist/assets/themes/bootstrap/scss/bootstrap-grid.scss +62 -0
  214. package/dist/assets/themes/bootstrap/scss/bootstrap-reboot.scss +10 -0
  215. package/dist/assets/themes/bootstrap/scss/bootstrap-utilities.scss +19 -0
  216. package/dist/assets/themes/bootstrap/scss/bootstrap.scss +52 -0
  217. package/dist/assets/themes/bootstrap/scss/forms/_floating-labels.scss +97 -0
  218. package/dist/assets/themes/bootstrap/scss/forms/_form-check.scss +189 -0
  219. package/dist/assets/themes/bootstrap/scss/forms/_form-control.scss +214 -0
  220. package/dist/assets/themes/bootstrap/scss/forms/_form-range.scss +91 -0
  221. package/dist/assets/themes/bootstrap/scss/forms/_form-select.scss +80 -0
  222. package/dist/assets/themes/bootstrap/scss/forms/_form-text.scss +11 -0
  223. package/dist/assets/themes/bootstrap/scss/forms/_input-group.scss +132 -0
  224. package/dist/assets/themes/bootstrap/scss/forms/_labels.scss +36 -0
  225. package/dist/assets/themes/bootstrap/scss/forms/_validation.scss +12 -0
  226. package/dist/assets/themes/bootstrap/scss/helpers/_clearfix.scss +3 -0
  227. package/dist/assets/themes/bootstrap/scss/helpers/_color-bg.scss +7 -0
  228. package/dist/assets/themes/bootstrap/scss/helpers/_colored-links.scss +30 -0
  229. package/dist/assets/themes/bootstrap/scss/helpers/_focus-ring.scss +5 -0
  230. package/dist/assets/themes/bootstrap/scss/helpers/_icon-link.scss +25 -0
  231. package/dist/assets/themes/bootstrap/scss/helpers/_position.scss +36 -0
  232. package/dist/assets/themes/bootstrap/scss/helpers/_ratio.scss +26 -0
  233. package/dist/assets/themes/bootstrap/scss/helpers/_stacks.scss +15 -0
  234. package/dist/assets/themes/bootstrap/scss/helpers/_stretched-link.scss +15 -0
  235. package/dist/assets/themes/bootstrap/scss/helpers/_text-truncation.scss +7 -0
  236. package/dist/assets/themes/bootstrap/scss/helpers/_visually-hidden.scss +8 -0
  237. package/dist/assets/themes/bootstrap/scss/helpers/_vr.scss +8 -0
  238. package/dist/assets/themes/bootstrap/scss/mixins/_alert.scss +18 -0
  239. package/dist/assets/themes/bootstrap/scss/mixins/_backdrop.scss +14 -0
  240. package/dist/assets/themes/bootstrap/scss/mixins/_banner.scss +7 -0
  241. package/dist/assets/themes/bootstrap/scss/mixins/_border-radius.scss +78 -0
  242. package/dist/assets/themes/bootstrap/scss/mixins/_box-shadow.scss +18 -0
  243. package/dist/assets/themes/bootstrap/scss/mixins/_breakpoints.scss +127 -0
  244. package/dist/assets/themes/bootstrap/scss/mixins/_buttons.scss +70 -0
  245. package/dist/assets/themes/bootstrap/scss/mixins/_caret.scss +69 -0
  246. package/dist/assets/themes/bootstrap/scss/mixins/_clearfix.scss +9 -0
  247. package/dist/assets/themes/bootstrap/scss/mixins/_color-mode.scss +21 -0
  248. package/dist/assets/themes/bootstrap/scss/mixins/_color-scheme.scss +7 -0
  249. package/dist/assets/themes/bootstrap/scss/mixins/_container.scss +11 -0
  250. package/dist/assets/themes/bootstrap/scss/mixins/_deprecate.scss +10 -0
  251. package/dist/assets/themes/bootstrap/scss/mixins/_forms.scss +163 -0
  252. package/dist/assets/themes/bootstrap/scss/mixins/_gradients.scss +47 -0
  253. package/dist/assets/themes/bootstrap/scss/mixins/_grid.scss +151 -0
  254. package/dist/assets/themes/bootstrap/scss/mixins/_image.scss +16 -0
  255. package/dist/assets/themes/bootstrap/scss/mixins/_list-group.scss +26 -0
  256. package/dist/assets/themes/bootstrap/scss/mixins/_lists.scss +7 -0
  257. package/dist/assets/themes/bootstrap/scss/mixins/_pagination.scss +10 -0
  258. package/dist/assets/themes/bootstrap/scss/mixins/_reset-text.scss +17 -0
  259. package/dist/assets/themes/bootstrap/scss/mixins/_resize.scss +6 -0
  260. package/dist/assets/themes/bootstrap/scss/mixins/_table-variants.scss +24 -0
  261. package/dist/assets/themes/bootstrap/scss/mixins/_text-truncate.scss +8 -0
  262. package/dist/assets/themes/bootstrap/scss/mixins/_transition.scss +26 -0
  263. package/dist/assets/themes/bootstrap/scss/mixins/_utilities.scss +97 -0
  264. package/dist/assets/themes/bootstrap/scss/mixins/_visually-hidden.scss +38 -0
  265. package/dist/assets/themes/bootstrap/scss/tests/jasmine.js +16 -0
  266. package/dist/assets/themes/bootstrap/scss/tests/mixins/_auto-import-of-variables-dark.test.scss +7 -0
  267. package/dist/assets/themes/bootstrap/scss/tests/mixins/_color-modes.test.scss +69 -0
  268. package/dist/assets/themes/bootstrap/scss/tests/mixins/_media-query-color-mode-full.test.scss +8 -0
  269. package/dist/assets/themes/bootstrap/scss/tests/mixins/_utilities.test.scss +393 -0
  270. package/dist/assets/themes/bootstrap/scss/tests/sass-true/register.js +14 -0
  271. package/dist/assets/themes/bootstrap/scss/tests/sass-true/runner.js +17 -0
  272. package/dist/assets/themes/bootstrap/scss/tests/utilities/_api.test.scss +75 -0
  273. package/dist/assets/themes/bootstrap/scss/utilities/_api.scss +47 -0
  274. package/dist/assets/themes/bootstrap/scss/vendor/_rfs.scss +348 -0
  275. package/dist/assets/themes/classy/README.md +89 -0
  276. package/dist/assets/themes/classy/_config.scss +245 -0
  277. package/dist/assets/themes/classy/_theme.js +31 -0
  278. package/dist/assets/themes/classy/_theme.scss +57 -0
  279. package/dist/assets/themes/classy/css/app/_panels.scss +183 -0
  280. package/dist/assets/themes/classy/css/base/_fonts.scss +79 -0
  281. package/dist/assets/themes/classy/css/base/_no-raise.scss +49 -0
  282. package/dist/assets/themes/classy/css/base/_root.scss +113 -0
  283. package/dist/assets/themes/classy/css/base/_typography.scss +154 -0
  284. package/dist/assets/themes/classy/css/base/_utilities.scss +284 -0
  285. package/dist/assets/themes/classy/css/components/_alerts.scss +51 -0
  286. package/dist/assets/themes/classy/css/components/_badges.scss +46 -0
  287. package/dist/assets/themes/classy/css/components/_buttons.scss +175 -0
  288. package/dist/assets/themes/classy/css/components/_cards.scss +41 -0
  289. package/dist/assets/themes/classy/css/components/_dropdowns.scss +48 -0
  290. package/dist/assets/themes/classy/css/components/_forms.scss +171 -0
  291. package/dist/assets/themes/classy/css/components/_receipt.scss +45 -0
  292. package/dist/assets/themes/classy/css/layout/_footer.scss +298 -0
  293. package/dist/assets/themes/classy/css/layout/_general.scss +209 -0
  294. package/dist/assets/themes/classy/css/layout/_nav.scss +154 -0
  295. package/dist/assets/themes/classy/css/layout/_shell.scss +458 -0
  296. package/dist/assets/themes/classy/css/marketing/_bento.scss +238 -0
  297. package/dist/assets/themes/classy/css/marketing/_content.scss +1328 -0
  298. package/dist/assets/themes/classy/css/marketing/_downloads.scss +246 -0
  299. package/dist/assets/themes/classy/css/marketing/_hero.scss +311 -0
  300. package/dist/assets/themes/classy/css/marketing/_sections.scss +649 -0
  301. package/dist/assets/themes/classy/css/pages/404/index.scss +67 -0
  302. package/dist/assets/themes/classy/css/pages/_auth.scss +235 -0
  303. package/dist/assets/themes/classy/css/pages/admin/firebase/index.scss +22 -0
  304. package/dist/assets/themes/classy/css/pages/blog/[slug].scss +56 -0
  305. package/dist/assets/themes/classy/css/pages/cookies.scss +2 -0
  306. package/dist/assets/themes/classy/css/pages/dashboard/account/index.scss +31 -0
  307. package/dist/assets/themes/classy/css/pages/feedback/index.scss +39 -0
  308. package/dist/assets/themes/classy/css/pages/legal/_document.scss +192 -0
  309. package/dist/assets/themes/classy/css/pages/payment/checkout/index.scss +585 -0
  310. package/dist/assets/themes/classy/css/pages/payment/confirmation/index.scss +9 -0
  311. package/dist/assets/themes/classy/css/pages/privacy.scss +2 -0
  312. package/dist/assets/themes/classy/css/pages/status/index.scss +164 -0
  313. package/dist/assets/themes/classy/css/pages/terms.scss +2 -0
  314. package/dist/assets/themes/classy/css/pages/updates/index.scss +128 -0
  315. package/dist/assets/themes/classy/fonts/inter-italic-latin-ext.woff2 +0 -0
  316. package/dist/assets/themes/classy/fonts/inter-italic-latin.woff2 +0 -0
  317. package/dist/assets/themes/classy/fonts/inter-normal-latin-ext.woff2 +0 -0
  318. package/dist/assets/themes/classy/fonts/inter-normal-latin.woff2 +0 -0
  319. package/dist/assets/themes/classy/fonts/newsreader-italic-latin-ext.woff2 +0 -0
  320. package/dist/assets/themes/classy/fonts/newsreader-italic-latin.woff2 +0 -0
  321. package/dist/assets/themes/classy/fonts/newsreader-normal-latin-ext.woff2 +0 -0
  322. package/dist/assets/themes/classy/fonts/newsreader-normal-latin.woff2 +0 -0
  323. package/dist/assets/themes/classy/js/hero-demo-form.js +56 -0
  324. package/dist/assets/themes/neobrutalism/README.md +118 -0
  325. package/dist/assets/themes/neobrutalism/_config.scss +139 -0
  326. package/dist/assets/themes/neobrutalism/_layouts/frontend/core/base.html +30 -0
  327. package/dist/assets/themes/neobrutalism/_layouts/frontend/pages/index.html +275 -0
  328. package/dist/assets/themes/neobrutalism/_theme.js +24 -0
  329. package/dist/assets/themes/neobrutalism/_theme.scss +61 -0
  330. package/dist/assets/themes/neobrutalism/css/base/_mixins.scss +46 -0
  331. package/dist/assets/themes/neobrutalism/css/base/_root.scss +169 -0
  332. package/dist/assets/themes/neobrutalism/css/base/_typography.scss +61 -0
  333. package/dist/assets/themes/neobrutalism/css/base/_utilities.scss +89 -0
  334. package/dist/assets/themes/neobrutalism/css/components/_billing-toggle.scss +74 -0
  335. package/dist/assets/themes/neobrutalism/css/components/_buttons.scss +148 -0
  336. package/dist/assets/themes/neobrutalism/css/components/_cards.scss +47 -0
  337. package/dist/assets/themes/neobrutalism/css/components/_forms.scss +88 -0
  338. package/dist/assets/themes/neobrutalism/css/components/_marketing.scss +244 -0
  339. package/dist/assets/themes/neobrutalism/css/layout/_general.scss +213 -0
  340. package/dist/assets/themes/neobrutalism/css/layout/_navigation.scss +171 -0
  341. package/dist/assets/themes/neobrutalism/css/pages/index.scss +203 -0
  342. package/dist/assets/themes/neobrutalism/css/pages/pricing/index.scss +58 -0
  343. package/dist/assets/themes/neobrutalism/css/pages/test/libraries/layers/index.scss +7 -0
  344. package/dist/assets/themes/neobrutalism/js/pages/test/libraries/layers/index.js +9 -0
  345. package/dist/assets/themes/newsflash/README.md +117 -0
  346. package/dist/assets/themes/newsflash/_components/heading/lede/component.html +14 -0
  347. package/dist/assets/themes/newsflash/_components/heading/lede/component.json5 +23 -0
  348. package/dist/assets/themes/newsflash/_components/heading/rule-head/component.html +17 -0
  349. package/dist/assets/themes/newsflash/_components/heading/rule-head/component.json5 +23 -0
  350. package/dist/assets/themes/newsflash/_components/news/byline/component.html +12 -0
  351. package/dist/assets/themes/newsflash/_components/news/byline/component.json5 +18 -0
  352. package/dist/assets/themes/newsflash/_components/news/story-card/component.html +25 -0
  353. package/dist/assets/themes/newsflash/_components/news/story-card/component.json5 +21 -0
  354. package/dist/assets/themes/newsflash/_config.scss +147 -0
  355. package/dist/assets/themes/newsflash/_layouts/frontend/core/base.html +51 -0
  356. package/dist/assets/themes/newsflash/_layouts/frontend/pages/blog/index.html +299 -0
  357. package/dist/assets/themes/newsflash/_layouts/frontend/pages/blog/post.html +235 -0
  358. package/dist/assets/themes/newsflash/_layouts/frontend/pages/index.html +328 -0
  359. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.html +39 -0
  360. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.json5 +49 -0
  361. package/dist/assets/themes/newsflash/_sections/marketing/newsletter-cta/section.html +103 -0
  362. package/dist/assets/themes/newsflash/_sections/marketing/newsletter-cta/section.json5 +50 -0
  363. package/dist/assets/themes/newsflash/_sections/marketing/rundown/section.html +28 -0
  364. package/dist/assets/themes/newsflash/_sections/marketing/rundown/section.json5 +40 -0
  365. package/dist/assets/themes/newsflash/_theme.js +24 -0
  366. package/dist/assets/themes/newsflash/_theme.scss +70 -0
  367. package/dist/assets/themes/newsflash/css/base/_fonts.scss +80 -0
  368. package/dist/assets/themes/newsflash/css/base/_mixins.scss +51 -0
  369. package/dist/assets/themes/newsflash/css/base/_root.scss +257 -0
  370. package/dist/assets/themes/newsflash/css/base/_typography.scss +49 -0
  371. package/dist/assets/themes/newsflash/css/base/_utilities.scss +122 -0
  372. package/dist/assets/themes/newsflash/css/components/_badges.scss +65 -0
  373. package/dist/assets/themes/newsflash/css/components/_billing-toggle.scss +61 -0
  374. package/dist/assets/themes/newsflash/css/components/_buttons.scss +139 -0
  375. package/dist/assets/themes/newsflash/css/components/_cards.scss +47 -0
  376. package/dist/assets/themes/newsflash/css/components/_editorial.scss +173 -0
  377. package/dist/assets/themes/newsflash/css/components/_forms.scss +75 -0
  378. package/dist/assets/themes/newsflash/css/components/_infinite-scroll.scss +102 -0
  379. package/dist/assets/themes/newsflash/css/components/_panels.scss +156 -0
  380. package/dist/assets/themes/newsflash/css/components/_ticker.scss +67 -0
  381. package/dist/assets/themes/newsflash/css/layout/_general.scss +330 -0
  382. package/dist/assets/themes/newsflash/css/layout/_navigation.scss +168 -0
  383. package/dist/assets/themes/newsflash/css/pages/404/index.scss +79 -0
  384. package/dist/assets/themes/newsflash/css/pages/blog/[slug].scss +186 -0
  385. package/dist/assets/themes/newsflash/css/pages/blog/index.scss +17 -0
  386. package/dist/assets/themes/newsflash/css/pages/index.scss +157 -0
  387. package/dist/assets/themes/newsflash/css/pages/pricing/index.scss +55 -0
  388. package/dist/assets/themes/newsflash/css/pages/test/libraries/layers/index.scss +7 -0
  389. package/dist/assets/themes/newsflash/fonts/fraunces-italic-latin-ext.woff2 +0 -0
  390. package/dist/assets/themes/newsflash/fonts/fraunces-italic-latin.woff2 +0 -0
  391. package/dist/assets/themes/newsflash/fonts/fraunces-normal-latin-ext.woff2 +0 -0
  392. package/dist/assets/themes/newsflash/fonts/fraunces-normal-latin.woff2 +0 -0
  393. package/dist/assets/themes/newsflash/fonts/schibsted-grotesk-italic-latin-ext.woff2 +0 -0
  394. package/dist/assets/themes/newsflash/fonts/schibsted-grotesk-italic-latin.woff2 +0 -0
  395. package/dist/assets/themes/newsflash/fonts/schibsted-grotesk-normal-latin-ext.woff2 +0 -0
  396. package/dist/assets/themes/newsflash/fonts/schibsted-grotesk-normal-latin.woff2 +0 -0
  397. package/dist/assets/themes/newsflash/js/pages/blog/[slug].js +29 -0
  398. package/dist/assets/themes/newsflash/js/pages/test/libraries/layers/index.js +9 -0
  399. package/dist/build.js +249 -0
  400. package/dist/cli-run.js +34 -0
  401. package/dist/cli.js +49 -0
  402. package/dist/commands/build.js +32 -0
  403. package/dist/commands/cdp/capture.js +91 -0
  404. package/dist/commands/cdp/client.js +140 -0
  405. package/dist/commands/cdp/eval.js +20 -0
  406. package/dist/commands/cdp/quit.js +84 -0
  407. package/dist/commands/cdp/relaunch.js +62 -0
  408. package/dist/commands/cdp/shot.js +20 -0
  409. package/dist/commands/cdp/status.js +38 -0
  410. package/dist/commands/cdp/theme.js +21 -0
  411. package/dist/commands/cdp.js +53 -0
  412. package/dist/commands/clean.js +34 -0
  413. package/dist/commands/deploy.js +75 -0
  414. package/dist/commands/finalize-release.js +238 -0
  415. package/dist/commands/install.js +37 -0
  416. package/dist/commands/launch.js +150 -0
  417. package/dist/commands/lib/dependencies.js +35 -0
  418. package/dist/commands/lib/deploy-precheck.js +117 -0
  419. package/dist/commands/lib/ensure-target.js +300 -0
  420. package/dist/commands/logs.js +185 -0
  421. package/dist/commands/package.js +31 -0
  422. package/dist/commands/publish.js +34 -0
  423. package/dist/commands/push-secrets.js +141 -0
  424. package/dist/commands/release.js +286 -0
  425. package/dist/commands/runner.js +1874 -0
  426. package/dist/commands/sign-windows.js +563 -0
  427. package/dist/commands/test.js +121 -0
  428. package/dist/commands/update.js +21 -0
  429. package/dist/commands/validate-certs.js +238 -0
  430. package/dist/commands/version.js +9 -0
  431. package/dist/config/page-template.html +35 -0
  432. package/dist/defaults/.github/workflows/build.yml +276 -0
  433. package/dist/defaults/AGENTS.md +109 -0
  434. package/dist/defaults/CHANGELOG.md +15 -0
  435. package/dist/defaults/CLAUDE.md +1 -0
  436. package/dist/defaults/_.gitignore +24 -0
  437. package/dist/defaults/_mas/README.md +49 -0
  438. package/dist/defaults/_mas/entitlements.mas.inherit.plist +12 -0
  439. package/dist/defaults/_mas/entitlements.mas.loginhelper.plist +8 -0
  440. package/dist/defaults/_mas/entitlements.mas.plist +33 -0
  441. package/dist/defaults/config/certs/.gitkeep +0 -0
  442. package/dist/defaults/config/certs/README.md +60 -0
  443. package/dist/defaults/config/icons/macos/dmg.png +0 -0
  444. package/dist/defaults/config/icons/macos/icon.png +0 -0
  445. package/dist/defaults/config/icons/macos/tray.png +0 -0
  446. package/dist/defaults/config/icons/windows/icon.png +0 -0
  447. package/dist/defaults/config/omega.json5 +279 -0
  448. package/dist/defaults/docs/README.md +17 -0
  449. package/dist/defaults/gulpfile.js +6 -0
  450. package/dist/defaults/hooks/build/post.js +9 -0
  451. package/dist/defaults/hooks/build/pre.js +9 -0
  452. package/dist/defaults/hooks/notarize/post.js +16 -0
  453. package/dist/defaults/hooks/release/post.js +9 -0
  454. package/dist/defaults/hooks/release/pre.js +9 -0
  455. package/dist/defaults/src/assets/js/components/about/index.js +10 -0
  456. package/dist/defaults/src/assets/js/components/main/index.js +14 -0
  457. package/dist/defaults/src/assets/js/components/settings/index.js +10 -0
  458. package/dist/defaults/src/assets/scss/main.scss +34 -0
  459. package/dist/defaults/src/assets/scss/pages/about.scss +3 -0
  460. package/dist/defaults/src/assets/scss/pages/main.scss +4 -0
  461. package/dist/defaults/src/assets/scss/pages/settings.scss +3 -0
  462. package/dist/defaults/src/integrations/context-menu/index.js +60 -0
  463. package/dist/defaults/src/integrations/menu/index.js +58 -0
  464. package/dist/defaults/src/integrations/tray/index.js +60 -0
  465. package/dist/defaults/src/main.js +78 -0
  466. package/dist/defaults/src/preload.js +15 -0
  467. package/dist/defaults/src/views/about/index.html +3 -0
  468. package/dist/defaults/src/views/main/index.html +8 -0
  469. package/dist/defaults/src/views/settings/index.html +3 -0
  470. package/dist/defaults/test/README.md +41 -0
  471. package/dist/defaults/test/_init.js +10 -0
  472. package/dist/gulp/main.js +144 -0
  473. package/dist/gulp/tasks/audit.js +82 -0
  474. package/dist/gulp/tasks/build-config.js +445 -0
  475. package/dist/gulp/tasks/bundle.js +451 -0
  476. package/dist/gulp/tasks/defaults.js +20 -0
  477. package/dist/gulp/tasks/distribute.js +8 -0
  478. package/dist/gulp/tasks/html.js +95 -0
  479. package/dist/gulp/tasks/package-quick.js +102 -0
  480. package/dist/gulp/tasks/package.js +53 -0
  481. package/dist/gulp/tasks/release.js +57 -0
  482. package/dist/gulp/tasks/sass.js +82 -0
  483. package/dist/gulp/tasks/serve.js +191 -0
  484. package/dist/hooks/notarize.js +70 -0
  485. package/dist/index.js +14 -0
  486. package/dist/lib/_menu-mixin.js +199 -0
  487. package/dist/lib/analytics.js +360 -0
  488. package/dist/lib/app-state.js +220 -0
  489. package/dist/lib/auth-flow.js +160 -0
  490. package/dist/lib/auth-persistence.js +207 -0
  491. package/dist/lib/auto-updater.js +752 -0
  492. package/dist/lib/client-bridge.js +371 -0
  493. package/dist/lib/context-menu.js +302 -0
  494. package/dist/lib/context.js +162 -0
  495. package/dist/lib/deep-link.js +370 -0
  496. package/dist/lib/fontawesome.js +203 -0
  497. package/dist/lib/ipc.js +169 -0
  498. package/dist/lib/logger-lite.js +259 -0
  499. package/dist/lib/logger.js +4 -0
  500. package/dist/lib/menu.js +521 -0
  501. package/dist/lib/protocol.js +85 -0
  502. package/dist/lib/remote-config.js +224 -0
  503. package/dist/lib/remote-scripts.js +158 -0
  504. package/dist/lib/restart-manager/index.js +550 -0
  505. package/dist/lib/restart-manager/install.js +353 -0
  506. package/dist/lib/restart-manager/protocol.js +274 -0
  507. package/dist/lib/sign-helpers/auto-unlock.js +95 -0
  508. package/dist/lib/sign-helpers/entitlements.js +98 -0
  509. package/dist/lib/sign-helpers/resolve-icons.js +171 -0
  510. package/dist/lib/sign-helpers/sign-events.js +75 -0
  511. package/dist/lib/sign-helpers/update-info.js +186 -0
  512. package/dist/lib/startup.js +214 -0
  513. package/dist/lib/storage.js +193 -0
  514. package/dist/lib/templating.js +85 -0
  515. package/dist/lib/theme.js +173 -0
  516. package/dist/lib/tray.js +414 -0
  517. package/dist/lib/usage.js +141 -0
  518. package/dist/lib/window-manager.js +509 -0
  519. package/dist/main.js +542 -0
  520. package/dist/omega-bin.js +6 -0
  521. package/dist/preload.js +241 -0
  522. package/dist/renderer.js +456 -0
  523. package/dist/test/assert.js +4 -0
  524. package/dist/test/fixtures/consumer-app/config/omega.json5 +39 -0
  525. package/dist/test/fixtures/consumer-app/package.json +13 -0
  526. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +11 -0
  527. package/dist/test/fixtures/consumer-app/src/assets/scss/main.scss +3 -0
  528. package/dist/test/fixtures/consumer-app/src/main.js +17 -0
  529. package/dist/test/fixtures/consumer-app/src/preload.js +4 -0
  530. package/dist/test/fixtures/consumer-app/src/views/main/index.html +4 -0
  531. package/dist/test/harness/boot-entry.js +237 -0
  532. package/dist/test/harness/main-entry.js +494 -0
  533. package/dist/test/harness/package.json +6 -0
  534. package/dist/test/harness/renderer-entry.js +173 -0
  535. package/dist/test/harness/renderer-preload.js +310 -0
  536. package/dist/test/harness/renderer.html +12 -0
  537. package/dist/test/index.js +65 -0
  538. package/dist/test/runner.js +99 -0
  539. package/dist/test/runners/boot.js +414 -0
  540. package/dist/test/runners/electron.js +92 -0
  541. package/dist/test/runners/render-event.js +46 -0
  542. package/dist/test/suites/boot/build-isolation.test.js +54 -0
  543. package/dist/test/suites/boot/consumer-app-boots.test.js +99 -0
  544. package/dist/test/suites/boot/restart-manager.test.js +38 -0
  545. package/dist/test/suites/boot/storage-bundled.test.js +59 -0
  546. package/dist/test/suites/boot/theme.test.js +68 -0
  547. package/dist/test/suites/boot/view-suite.test.js +47 -0
  548. package/dist/test/suites/build/attach-log-file.test.js +99 -0
  549. package/dist/test/suites/build/audit.test.js +271 -0
  550. package/dist/test/suites/build/boot-fixture.test.js +48 -0
  551. package/dist/test/suites/build/build-config.test.js +457 -0
  552. package/dist/test/suites/build/build-verbs.test.js +156 -0
  553. package/dist/test/suites/build/build-workflow-jobs.test.js +55 -0
  554. package/dist/test/suites/build/cdp.test.js +89 -0
  555. package/dist/test/suites/build/cli.test.js +135 -0
  556. package/dist/test/suites/build/config-schema.test.js +133 -0
  557. package/dist/test/suites/build/defaults-scaffold.test.js +192 -0
  558. package/dist/test/suites/build/deliver-certs.test.js +95 -0
  559. package/dist/test/suites/build/derive-signing-env.test.js +122 -0
  560. package/dist/test/suites/build/electron-node-version.test.js +49 -0
  561. package/dist/test/suites/build/electron-targets.test.js +92 -0
  562. package/dist/test/suites/build/ensure-target.test.js +131 -0
  563. package/dist/test/suites/build/entitlements.test.js +107 -0
  564. package/dist/test/suites/build/env-delivery.test.js +166 -0
  565. package/dist/test/suites/build/env-watch.test.js +86 -0
  566. package/dist/test/suites/build/exports.test.js +63 -0
  567. package/dist/test/suites/build/finalize-release.test.js +152 -0
  568. package/dist/test/suites/build/format-fetch-error.test.js +85 -0
  569. package/dist/test/suites/build/framework-deps.test.js +173 -0
  570. package/dist/test/suites/build/get-config.test.js +137 -0
  571. package/dist/test/suites/build/github-utils.test.js +54 -0
  572. package/dist/test/suites/build/icon-sheet.test.js +62 -0
  573. package/dist/test/suites/build/license-stamp.test.js +63 -0
  574. package/dist/test/suites/build/logger-lite.test.js +148 -0
  575. package/dist/test/suites/build/logs-command.test.js +226 -0
  576. package/dist/test/suites/build/manager.test.js +193 -0
  577. package/dist/test/suites/build/menu-mixin.test.js +130 -0
  578. package/dist/test/suites/build/merge-line-files.test.js +230 -0
  579. package/dist/test/suites/build/omega-shell.test.js +94 -0
  580. package/dist/test/suites/build/omega-tokens.test.js +64 -0
  581. package/dist/test/suites/build/project-scripts-deps.test.js +90 -0
  582. package/dist/test/suites/build/push-secrets.test.js +226 -0
  583. package/dist/test/suites/build/release-pipeline.test.js +365 -0
  584. package/dist/test/suites/build/renderer-auth-bridge.test.js +158 -0
  585. package/dist/test/suites/build/renderer-entry-key.test.js +37 -0
  586. package/dist/test/suites/build/renderer-node-shims.test.js +125 -0
  587. package/dist/test/suites/build/resolve-icons.test.js +296 -0
  588. package/dist/test/suites/build/resolve-signing-cert.test.js +342 -0
  589. package/dist/test/suites/build/restart-manager-install.test.js +178 -0
  590. package/dist/test/suites/build/restart-manager-protocol.test.js +158 -0
  591. package/dist/test/suites/build/run-consumer-hook.test.js +137 -0
  592. package/dist/test/suites/build/runner.test.js +2599 -0
  593. package/dist/test/suites/build/sanitize-signing-env.test.js +98 -0
  594. package/dist/test/suites/build/sanitize-url.test.js +78 -0
  595. package/dist/test/suites/build/secure-remote-url.test.js +42 -0
  596. package/dist/test/suites/build/sentry.test.js +89 -0
  597. package/dist/test/suites/build/serve-resolve-electron.test.js +58 -0
  598. package/dist/test/suites/build/setup-scripts.test.js +172 -0
  599. package/dist/test/suites/build/sign-events.test.js +90 -0
  600. package/dist/test/suites/build/sign-windows-e2e.test.js +302 -0
  601. package/dist/test/suites/build/sign-windows.test.js +313 -0
  602. package/dist/test/suites/build/strip-dev-blocks.test.js +116 -0
  603. package/dist/test/suites/build/templating.test.js +95 -0
  604. package/dist/test/suites/build/test-discovery.test.js +64 -0
  605. package/dist/test/suites/build/test-stealth.test.js +83 -0
  606. package/dist/test/suites/build/test-target-no-match.test.js +119 -0
  607. package/dist/test/suites/build/update-info.test.js +224 -0
  608. package/dist/test/suites/build/url-helpers.test.js +289 -0
  609. package/dist/test/suites/build/validate-certs.test.js +103 -0
  610. package/dist/test/suites/build/validate-config.test.js +90 -0
  611. package/dist/test/suites/build/verb-logs.test.js +43 -0
  612. package/dist/test/suites/build/wave5-pins.test.js +35 -0
  613. package/dist/test/suites/main/analytics.test.js +364 -0
  614. package/dist/test/suites/main/app-state.test.js +197 -0
  615. package/dist/test/suites/main/auth-flow.test.js +196 -0
  616. package/dist/test/suites/main/auth-persistence.test.js +167 -0
  617. package/dist/test/suites/main/auto-updater.test.js +1138 -0
  618. package/dist/test/suites/main/boot-sequence.test.js +77 -0
  619. package/dist/test/suites/main/client-bridge.integration.test.js +134 -0
  620. package/dist/test/suites/main/client-bridge.test.js +249 -0
  621. package/dist/test/suites/main/context-menu.test.js +365 -0
  622. package/dist/test/suites/main/context.test.js +155 -0
  623. package/dist/test/suites/main/deep-link.test.js +288 -0
  624. package/dist/test/suites/main/epipe-handler.test.js +40 -0
  625. package/dist/test/suites/main/fontawesome.test.js +147 -0
  626. package/dist/test/suites/main/harness-cdp.test.js +49 -0
  627. package/dist/test/suites/main/ipc.test.js +165 -0
  628. package/dist/test/suites/main/menu.test.js +419 -0
  629. package/dist/test/suites/main/protocol.test.js +77 -0
  630. package/dist/test/suites/main/remote-config.test.js +189 -0
  631. package/dist/test/suites/main/remote-scripts.test.js +276 -0
  632. package/dist/test/suites/main/restart-manager.test.js +328 -0
  633. package/dist/test/suites/main/startup-paths-and-ua.test.js +99 -0
  634. package/dist/test/suites/main/startup.test.js +129 -0
  635. package/dist/test/suites/main/stealth-window.test.js +74 -0
  636. package/dist/test/suites/main/storage.test.js +89 -0
  637. package/dist/test/suites/main/theme.test.js +137 -0
  638. package/dist/test/suites/main/tray.test.js +322 -0
  639. package/dist/test/suites/main/truncate-on-boot.test.js +22 -0
  640. package/dist/test/suites/main/url-helpers.test.js +429 -0
  641. package/dist/test/suites/main/usage.test.js +127 -0
  642. package/dist/test/suites/main/window-bounds.test.js +169 -0
  643. package/dist/test/suites/main/window-manager.test.js +233 -0
  644. package/dist/test/suites/renderer/analytics-bridge.test.js +128 -0
  645. package/dist/test/suites/renderer/cross-context-helpers.test.js +131 -0
  646. package/dist/test/suites/renderer/fontawesome.test.js +190 -0
  647. package/dist/test/suites/renderer/round-trip.test.js +107 -0
  648. package/dist/test/suites/renderer/theme.test.js +118 -0
  649. package/dist/test/suites/renderer/tooltips.test.js +166 -0
  650. package/dist/test/suites/renderer/verts.test.js +100 -0
  651. package/dist/test/suites/renderer/window-em-surface.test.js +107 -0
  652. package/dist/test/utils/dist-snapshot.js +36 -0
  653. package/dist/test/utils/extended-mode-warning.js +13 -0
  654. package/dist/utils/app-root.js +28 -0
  655. package/dist/utils/attach-log-file.js +4 -0
  656. package/dist/utils/build-pipeline.js +58 -0
  657. package/dist/utils/deliver-certs.js +69 -0
  658. package/dist/utils/derive-signing-env.js +56 -0
  659. package/dist/utils/dist-root.js +21 -0
  660. package/dist/utils/electron-node-version.js +48 -0
  661. package/dist/utils/electron-targets.js +72 -0
  662. package/dist/utils/format-fetch-error.js +38 -0
  663. package/dist/utils/github.js +73 -0
  664. package/dist/utils/load-consumer-file.js +26 -0
  665. package/dist/utils/merge-line-files.js +5 -0
  666. package/dist/utils/mode-helpers.js +147 -0
  667. package/dist/utils/resolve-signing-cert.js +175 -0
  668. package/dist/utils/run-consumer-hook.js +38 -0
  669. package/dist/utils/runner-env.js +687 -0
  670. package/dist/utils/safe-install.js +4 -0
  671. package/dist/utils/sanitize-signing-env.js +45 -0
  672. package/dist/utils/sanitize-url.js +28 -0
  673. package/dist/utils/secure-remote-url.js +25 -0
  674. package/dist/utils/stealth-window.js +37 -0
  675. package/dist/utils/test-stealth.js +29 -0
  676. package/dist/utils/url-helpers.js +169 -0
  677. package/dist/vendor/account/engine.js +182 -0
  678. package/dist/vendor/account/features.js +220 -0
  679. package/dist/vendor/account/index.js +53 -0
  680. package/dist/vendor/account/schema.js +272 -0
  681. package/dist/vendor/account/subscription.js +38 -0
  682. package/dist/vendor/analytics/adapters/ga4.js +26 -0
  683. package/dist/vendor/analytics/adapters/meta.js +26 -0
  684. package/dist/vendor/analytics/adapters/resolve.js +130 -0
  685. package/dist/vendor/analytics/adapters/tiktok.js +27 -0
  686. package/dist/vendor/analytics/catalog.js +908 -0
  687. package/dist/vendor/analytics/consent.js +49 -0
  688. package/dist/vendor/analytics/core.js +141 -0
  689. package/dist/vendor/analytics/identity.js +136 -0
  690. package/dist/vendor/analytics/index.js +170 -0
  691. package/dist/vendor/analytics/logger.js +40 -0
  692. package/dist/vendor/analytics/transports/browser.js +110 -0
  693. package/dist/vendor/config/company.js +31 -0
  694. package/dist/vendor/config/defaults.js +173 -0
  695. package/dist/vendor/config/demo.js +18 -0
  696. package/dist/vendor/config/desktop-artifacts.js +110 -0
  697. package/dist/vendor/config/edit.js +769 -0
  698. package/dist/vendor/config/env-delivery.js +145 -0
  699. package/dist/vendor/config/env-rules.js +93 -0
  700. package/dist/vendor/config/env-schema.js +1078 -0
  701. package/dist/vendor/config/env.js +445 -0
  702. package/dist/vendor/config/hooks.js +97 -0
  703. package/dist/vendor/config/index.js +237 -0
  704. package/dist/vendor/config/instances.js +208 -0
  705. package/dist/vendor/config/load.js +490 -0
  706. package/dist/vendor/config/merge.js +68 -0
  707. package/dist/vendor/config/order.js +139 -0
  708. package/dist/vendor/config/ports.js +374 -0
  709. package/dist/vendor/config/providers.js +32 -0
  710. package/dist/vendor/config/repo.js +142 -0
  711. package/dist/vendor/config/retired-keys.js +430 -0
  712. package/dist/vendor/config/schema.js +1610 -0
  713. package/dist/vendor/config/secrets.js +50 -0
  714. package/dist/vendor/config/seed.js +34 -0
  715. package/dist/vendor/config/site-global.js +205 -0
  716. package/dist/vendor/config/validate.js +554 -0
  717. package/dist/vendor/config/winback.js +61 -0
  718. package/dist/vendor/devkit/actions-secrets.js +145 -0
  719. package/dist/vendor/devkit/attach-log-file.js +262 -0
  720. package/dist/vendor/devkit/bundle.js +292 -0
  721. package/dist/vendor/devkit/certs.js +199 -0
  722. package/dist/vendor/devkit/ci-workflows.js +520 -0
  723. package/dist/vendor/devkit/clean-dirs.js +28 -0
  724. package/dist/vendor/devkit/cli-router.js +156 -0
  725. package/dist/vendor/devkit/defaults-engine.js +372 -0
  726. package/dist/vendor/devkit/deploy-precheck.js +54 -0
  727. package/dist/vendor/devkit/deploy-record.js +180 -0
  728. package/dist/vendor/devkit/deploy.js +286 -0
  729. package/dist/vendor/devkit/empty-modules-plugin.js +64 -0
  730. package/dist/vendor/devkit/env-watch.js +132 -0
  731. package/dist/vendor/devkit/framework-deps.js +46 -0
  732. package/dist/vendor/devkit/license.js +155 -0
  733. package/dist/vendor/devkit/local.js +1905 -0
  734. package/dist/vendor/devkit/logger.js +128 -0
  735. package/dist/vendor/devkit/merge-line-files.js +296 -0
  736. package/dist/vendor/devkit/npm-registry.js +52 -0
  737. package/dist/vendor/devkit/omega-bin.js +345 -0
  738. package/dist/vendor/devkit/prompt.js +187 -0
  739. package/dist/vendor/devkit/safe-install.js +18 -0
  740. package/dist/vendor/devkit/scaffold-guard.js +96 -0
  741. package/dist/vendor/devkit/stop-signals.js +28 -0
  742. package/dist/vendor/devkit/strip-dev-blocks-plugin.js +29 -0
  743. package/dist/vendor/devkit/strip-dev-blocks.js +32 -0
  744. package/dist/vendor/devkit/target-dependencies.js +137 -0
  745. package/dist/vendor/devkit/target-secrets.js +165 -0
  746. package/dist/vendor/devkit/test/assert.js +120 -0
  747. package/dist/vendor/devkit/test/define-cases.js +104 -0
  748. package/dist/vendor/devkit/test/extended-mode-warning.js +22 -0
  749. package/dist/vendor/devkit/test/runner-core.js +554 -0
  750. package/dist/vendor/devkit/test/scope.js +162 -0
  751. package/dist/vendor/devkit/update.js +569 -0
  752. package/dist/vendor/monitoring/core.js +180 -0
  753. package/dist/vendor/monitoring/env.js +49 -0
  754. package/dist/vendor/monitoring/index.js +28 -0
  755. package/dist/vendor/monitoring/logger.js +39 -0
  756. package/dist/vendor/monitoring/main.js +99 -0
  757. package/dist/vendor/monitoring/preload.js +45 -0
  758. package/dist/vendor/monitoring/renderer.js +87 -0
  759. package/docs/analytics.md +140 -0
  760. package/docs/app-state.md +92 -0
  761. package/docs/audit.md +69 -0
  762. package/docs/auto-updater.md +243 -0
  763. package/docs/boot-sequence.md +39 -0
  764. package/docs/build-system.md +161 -0
  765. package/docs/cdp-debugging.md +169 -0
  766. package/docs/client-bridge.md +267 -0
  767. package/docs/common-mistakes.md +21 -0
  768. package/docs/config-schema.md +120 -0
  769. package/docs/context-menu.md +112 -0
  770. package/docs/context.md +81 -0
  771. package/docs/css.md +84 -0
  772. package/docs/deep-link.md +178 -0
  773. package/docs/environment-detection.md +104 -0
  774. package/docs/fontawesome.md +107 -0
  775. package/docs/hooks.md +87 -0
  776. package/docs/icons.md +79 -0
  777. package/docs/index.md +279 -0
  778. package/docs/installer-options.md +162 -0
  779. package/docs/ipc.md +61 -0
  780. package/docs/lib-modules.md +53 -0
  781. package/docs/logging.md +229 -0
  782. package/docs/menu.md +160 -0
  783. package/docs/releasing.md +229 -0
  784. package/docs/remote-config.md +118 -0
  785. package/docs/remote-scripts.md +144 -0
  786. package/docs/restart-manager.md +144 -0
  787. package/docs/runner.md +253 -0
  788. package/docs/sentry.md +97 -0
  789. package/docs/shared/agent-docs.md +89 -0
  790. package/docs/shared/analytics.md +612 -0
  791. package/docs/shared/brands.md +51 -0
  792. package/docs/shared/breaking-changes.md +497 -0
  793. package/docs/shared/config.md +1387 -0
  794. package/docs/shared/deploys.md +215 -0
  795. package/docs/shared/icons.md +201 -0
  796. package/docs/shared/local-dev.md +147 -0
  797. package/docs/shared/logging.md +202 -0
  798. package/docs/shared/monitoring.md +153 -0
  799. package/docs/shared/publishing.md +183 -0
  800. package/docs/shared/rulings.md +34 -0
  801. package/docs/shared/testing.md +147 -0
  802. package/docs/shared/theming.md +604 -0
  803. package/docs/shared/translation.md +291 -0
  804. package/docs/shared/updates.md +61 -0
  805. package/docs/signing.md +267 -0
  806. package/docs/startup.md +142 -0
  807. package/docs/storage.md +59 -0
  808. package/docs/templating.md +101 -0
  809. package/docs/test-boot-layer.md +157 -0
  810. package/docs/test-framework.md +357 -0
  811. package/docs/themes.md +135 -0
  812. package/docs/tooltips.md +99 -0
  813. package/docs/tray.md +164 -0
  814. package/docs/usage.md +58 -0
  815. package/docs/verts.md +62 -0
  816. package/docs/windows.md +149 -0
  817. package/package.json +184 -0
  818. package/scripts/sync-nvmrc.js +100 -0
@@ -0,0 +1,1874 @@
1
+ // `npx omega runner <subcommand>` — Windows EV-token signing runner manager.
2
+ //
3
+ // v1.2.36+: install runs entirely at user privilege (RUNNER_HOME is in
4
+ // %LOCALAPPDATA%, no admin prompts, no UAC), and at end of install every
5
+ // registered runner is brought online detached, so the terminal it was typed
6
+ // in stays free. Auto-restart at next logon is wired up via a .cmd file in the
7
+ // user's Startup folder.
8
+ //
9
+ // Subcommands:
10
+ // install Idempotent setup: download actions/runner, register one
11
+ // runner per admin org, write each Startup shortcut. Tears
12
+ // down any prior install first, so re-running is safe.
13
+ // config Re-ask every key of the box's own .env — saved values as
14
+ // the defaults — and the orgs it serves. How a value that
15
+ // is already there gets changed.
16
+ // register-org <org> Register the runner against one specific GH org.
17
+ // start Bring EVERY registered org's runner online, detached,
18
+ // in Startup-shortcut order. An org already alive is
19
+ // named and skipped; the terminal is never taken over.
20
+ // restart stop, wait for every listener to go, then start.
21
+ // stop Kill every Runner.Listener.exe under RUNNER_HOME.
22
+ // status Registered orgs, Startup shortcuts, live listeners.
23
+ // uninstall Remove everything — legacy services and tasks, and the
24
+ // electron-manager era's em-runner install, included.
25
+ // self-update Force an immediate self-update of @omega.js/desktop (npm i -g @omega.js/desktop@latest).
26
+ // monitor Tail the JSONL signing event log, pretty-printed.
27
+ //
28
+ // Every subcommand except `self-update` and `monitor` refuses on non-Windows
29
+ // (set OMEGA_RUNNER_FORCE=1 to override — testing only).
30
+
31
+ const path = require('path');
32
+ const fs = require('fs');
33
+ const os = require('os');
34
+ const jetpack = require('fs-jetpack');
35
+
36
+ const Manager = new (require('../build.js'));
37
+ const logger = Manager.logger('runner');
38
+ const { safeInstall } = require('../utils/safe-install');
39
+
40
+ const RUNNER_LABELS = ['self-hosted', 'windows', 'ev-token'];
41
+
42
+ // One name, `omega-runner-<host>-<org>`, used on the GitHub side AND for the
43
+ // Startup-folder shortcut, so what you see in the org's runner list is what you
44
+ // see in the Startup folder. Capped at 64 chars — GitHub's limit on a runner name.
45
+ function ghRunnerName(org) {
46
+ return `omega-runner-${os.hostname().toLowerCase()}-${org.toLowerCase()}`.slice(0, 64);
47
+ }
48
+ // Runner files live under %LOCALAPPDATA%\omega-runner — a per-user path that
49
+ // doesn't need admin to read/write. v1.2.16-v1.2.35 used C:\actions-runners
50
+ // (root C:) which forced UAC elevation for every install/uninstall + spawned
51
+ // the runner in a separate elevated cmd window. With per-user storage we drop
52
+ // elevation entirely: install, register-org, start, and uninstall all run in
53
+ // the user's normal terminal, and the runners they bring up are detached, so
54
+ // the terminal comes straight back. Set OMEGA_RUNNER_HOME to override.
55
+ //
56
+ // The box's own configuration — GH_TOKEN, the orgs, the cert, the PIN, signtool
57
+ // — is `<home>\.env`, read here before anything looks at process.env, so every
58
+ // subcommand works from any directory (utils/runner-env.js).
59
+ const { defaultRunnerHome, runnerEnvFile, runnerLogFile, loadRunnerEnv, ensureRunnerConfig, reconfigureRunnerEnv, runnerEnvReport, parseRunnerOrgs, sameRunnerOrgs, isTestRun, isScratchRunnerHome, assertTestSafeRunnerHome, runnerPrivateHome, ensureRunnerPrivateHome, ensureRunnerDirEnv } = require('../utils/runner-env.js');
60
+ const attachLogFile = require('../utils/attach-log-file.js');
61
+ const RUNNER_HOME = process.env.OMEGA_RUNNER_HOME || defaultRunnerHome();
62
+ loadRunnerEnv({ home: RUNNER_HOME });
63
+
64
+ // The electron-manager era of this command (`em runner`) laid the same design
65
+ // down under other names: the home was %LOCALAPPDATA%\em-runner (C:\actions-runners
66
+ // before that), and the Startup shortcuts and the GitHub-side runners were
67
+ // `em-runner-<host>-<org>`. An upgraded box still has all of it — live, and
68
+ // picking up jobs — and none of it answers to the omega names, so `status`
69
+ // names it and `uninstall` (and the teardown `install` runs first) takes it
70
+ // down through the same per-org deregistration the omega home gets
71
+ // ([#337](https://github.com/Omega-JS-Stack/omega/issues/337)).
72
+ const LEGACY_RUNNER_PREFIX = 'em-runner-';
73
+
74
+ function legacyRunnerHomes(platform, env) {
75
+ platform = platform || process.platform;
76
+ env = env || process.env;
77
+ if (platform !== 'win32') return [];
78
+ // Windows paths whichever OS asks (the tests ask from a Mac).
79
+ const localAppData = env.LOCALAPPDATA || path.win32.join(os.homedir(), 'AppData', 'Local');
80
+ const systemDrive = env.SystemDrive || 'C:';
81
+ return [
82
+ path.win32.join(localAppData, 'em-runner'),
83
+ path.win32.join(`${systemDrive}${path.win32.sep}`, 'actions-runners'),
84
+ ];
85
+ }
86
+
87
+ // The legacy homes actually on disk — never the current home, wherever
88
+ // OMEGA_RUNNER_HOME points it. `exists` is injected by the tests.
89
+ function listLegacyRunnerHomes(options) {
90
+ const { platform, env, home, exists } = options || {};
91
+ const current = String(home || RUNNER_HOME).toLowerCase();
92
+ const isDir = exists || ((p) => jetpack.exists(p) === 'dir');
93
+ return legacyRunnerHomes(platform, env)
94
+ .filter((dir) => dir.toLowerCase() !== current)
95
+ .filter((dir) => isDir(dir));
96
+ }
97
+
98
+ // Each registration writes its config files at the top of its own directory, so
99
+ // one directory per org is the architecture, not a convenience.
100
+ function orgRunnerDir(org, home) {
101
+ return path.join(home || RUNNER_HOME, `actions-runner-${org.toLowerCase()}`);
102
+ }
103
+ const ACTIONS_RUNNER_VERSION = '2.319.1'; // pinned; bump intentionally
104
+
105
+ // The subcommands, and the two of them that run on any platform: self-update is
106
+ // an npm install, monitor only reads a log file. Every other one is the box's.
107
+ const SUBCOMMANDS = {
108
+ 'install': install,
109
+ 'config': configureRunner,
110
+ 'register-org': registerOrg,
111
+ 'start': startServices,
112
+ 'restart': restartServices,
113
+ 'stop': stopServices,
114
+ 'status': statusServices,
115
+ 'uninstall': uninstall,
116
+ 'self-update': selfUpdate,
117
+ 'monitor': monitor,
118
+ };
119
+ const ANY_PLATFORM_SUBCOMMANDS = new Set(['self-update', 'monitor']);
120
+
121
+ // The ones that CHANGE the machine: they register, deregister, download, delete,
122
+ // kill listeners, rewrite the box's own configuration, or run a global npm
123
+ // install. A test process may only point these at a scratch home
124
+ // ([#337](https://github.com/Omega-JS-Stack/omega/issues/337) — the suite once
125
+ // ran a real install against the box). `status` and `monitor` only read.
126
+ const MUTATING_SUBCOMMANDS = new Set(['install', 'config', 'register-org', 'start', 'restart', 'stop', 'uninstall', 'self-update']);
127
+
128
+ module.exports = async function (options) {
129
+ options = options || {};
130
+ const sub = (options._ && options._[1]) || 'status';
131
+
132
+ const handler = SUBCOMMANDS[sub];
133
+ if (!handler) {
134
+ logger.error(`Unknown subcommand: "${sub}". Try one of: ${Object.keys(SUBCOMMANDS).join(', ')}.`);
135
+ throw new Error(`Unknown runner subcommand: ${sub}`);
136
+ }
137
+
138
+ const home = options._home || RUNNER_HOME;
139
+
140
+ // Before ANYTHING else, including the platform gate: a test run may not act on
141
+ // this machine's real runner home. The box is precisely where the platform
142
+ // gate would have let it through, which is how 34 orgs got registered by a
143
+ // test once. The suites point OMEGA_RUNNER_HOME at a scratch; this is what
144
+ // happens when one forgets.
145
+ //
146
+ // BOTH homes, even though every subcommand now honours `_home`: the
147
+ // `loadRunnerEnv({ home: RUNNER_HOME })` at require time already read the real
148
+ // home's `.env` into process.env, so a run whose RUNNER_HOME is the box's is
149
+ // still carrying the box's token and PIN — a scratch `_home` alone does not
150
+ // make that call safe.
151
+ //
152
+ // And the STARTUP_DIR, because a home scopes nothing machine-wide: `uninstall`
153
+ // sweeps every `omega-runner-*.cmd` in that folder whatever home it was given,
154
+ // which is how the round-4 case — two scratch homes, both behaving — deleted
155
+ // the box's three real shortcuts. Off Windows it is null unless the seam names
156
+ // one, and `.filter(Boolean)` drops it.
157
+ if (MUTATING_SUBCOMMANDS.has(sub)) assertTestSafeRunnerHome(sub, home, RUNNER_HOME, STARTUP_DIR);
158
+
159
+ // The platform gate BEFORE the log file: off Windows every box subcommand
160
+ // refuses, and a refusal must not leave a runner home behind in whatever
161
+ // directory it was typed in (off Windows that home is `<cwd>/.gh-runners`).
162
+ // Each subcommand still calls this itself — they are also called directly.
163
+ if (!ANY_PLATFORM_SUBCOMMANDS.has(sub)) ensureWindows();
164
+
165
+ // The box keeps its own record of every runner command, in the runner home
166
+ // beside the install. Its OWN tee (not the process-wide singleton): a
167
+ // subcommand can run inside another verb's log, and an inner detach must not
168
+ // steal the outer one's file.
169
+ //
170
+ // `attachInCI` because this file is the BOX's, not the workspace's: a job
171
+ // running on the runner is exactly when the box's own record is wanted.
172
+ // `append` because more than one process writes it: `start` returns as soon as
173
+ // it has spawned the runners, and every `sign-windows` those listeners go on
174
+ // to run adds to the same file.
175
+ const tee = attachLogFile.createTee();
176
+ tee.attach(runnerLogFile(home), { env: options._env || process.env, attachInCI: true, append: true });
177
+
178
+ try {
179
+ return await handler(options);
180
+ } finally {
181
+ tee.detach();
182
+ }
183
+ };
184
+
185
+ // ─── install ────────────────────────────────────────────────────────────────────
186
+ async function install(options) {
187
+ options = options || {};
188
+ ensureWindows();
189
+ const home = options._home || RUNNER_HOME;
190
+ // The box's configuration comes first, and install asks for ALL of it — the
191
+ // same walk `config` runs, every key defaulted to what the box has now, then
192
+ // the orgs checkbox. A box that cannot answer does not install. The walk's
193
+ // org listing comes back with it (`adminOrgs`), so step 2 below does not pay
194
+ // for the same page of API calls twice.
195
+ const { adminOrgs } = await reconfigureRunnerEnv({
196
+ home,
197
+ logger,
198
+ prompt: options._prompt,
199
+ interactive: options._interactive,
200
+ discoverOrgs: discoverAdminOrgs,
201
+ registeredOrgs: readConfig(home).registeredOrgs || [],
202
+ });
203
+ // No admin anywhere in here: the runner home is per-user (%LOCALAPPDATA%\omega-runner),
204
+ // config.cmd registers without --runasservice (so no SCM access), and the
205
+ // Startup folder shortcut goes in the user's profile. Net: install runs in
206
+ // the calling terminal at normal user privilege.
207
+
208
+ // Idempotent by replacement: always tear down any prior install before starting —
209
+ // the omega one, or the electron-manager one an upgraded box still runs.
210
+ if (jetpack.exists(home) || listLegacyRunnerHomes({ home }).length > 0 || listLegacyRunnerStartupShortcuts().length > 0) {
211
+ logger.log(`Existing runner installation detected — uninstalling first for a clean re-install...`);
212
+ try {
213
+ await uninstall({ ...options, _home: home });
214
+ } catch (e) {
215
+ logger.warn(`Pre-install uninstall hit an error (continuing anyway): ${e.message}`);
216
+ }
217
+ }
218
+
219
+ logger.log(`Installing omega-runner under ${home}`);
220
+ jetpack.dir(home);
221
+
222
+ // 1. Download actions/runner ONCE into a template dir. Per-org dirs are cloned from this.
223
+ // Each actions-runner directory can only register against one org (it stores config files
224
+ // at the top of the dir), so multi-org = multi-directory + multi-service.
225
+ const templateDir = path.join(home, '_template');
226
+ await downloadActionsRunner(templateDir, home);
227
+
228
+ // 2. Resolve target orgs: filter list from OMEGA_RUNNER_ORGS if set, otherwise all admin orgs.
229
+ const allAdminOrgs = adminOrgs || await discoverAdminOrgs(); // null when the question never ran
230
+ if (allAdminOrgs.length === 0) {
231
+ logger.warn('Your GH_TOKEN has no orgs you can admin.');
232
+ }
233
+
234
+ const { orgs, filter, unmatched, filtered } = selectRunnerOrgs(allAdminOrgs, process.env.OMEGA_RUNNER_ORGS);
235
+ if (filtered) {
236
+ logger.log(`OMEGA_RUNNER_ORGS filter applied: ${orgs.length} matched (out of ${allAdminOrgs.length} admin orgs)`);
237
+ if (unmatched.length > 0) logger.warn(`OMEGA_RUNNER_ORGS lists ${unmatched.length} org(s) you don't admin: ${unmatched.join(', ')}`);
238
+ } else {
239
+ logger.log(`Detected ${orgs.length} admin org(s) (set OMEGA_RUNNER_ORGS in .env to install against a subset): ${orgs.join(', ')}`);
240
+ }
241
+
242
+ // 3. For each org: clone the template dir, run config.cmd inside it, write its
243
+ // Startup shortcut. The runner belongs to whoever is logged in — there is no
244
+ // service and no task to hand a separate account to.
245
+ logger.log(`Runners will belong to ${os.userInfo().username} (the logged-in user).`);
246
+ const succeeded = [];
247
+ const failedByReason = new Map();
248
+ for (const org of orgs) {
249
+ try {
250
+ await registerOrg({ ...options, _home: home, _: ['runner', 'register-org', org], _templateDir: templateDir });
251
+ succeeded.push(org);
252
+ } catch (e) {
253
+ const reason = e.message || String(e);
254
+ if (!failedByReason.has(reason)) failedByReason.set(reason, []);
255
+ failedByReason.get(reason).push(org);
256
+ }
257
+ }
258
+
259
+ // 4. There is no watcher service. The one that used to auto-register new
260
+ // admin orgs on a tick ran as NT AUTHORITY\NETWORK SERVICE in Session 0, so
261
+ // the runners it spawned could not see the user's cert store and the Startup
262
+ // shortcuts it wrote landed in the wrong profile. Adding a new org is a
263
+ // `mgr runner install` away; uninstall() still tears down any leftover
264
+ // watcher service from an older install.
265
+
266
+ // 5. Save install metadata.
267
+ saveConfig({
268
+ installedAt: new Date().toISOString(),
269
+ actionsRunnerVersion: ACTIONS_RUNNER_VERSION,
270
+ labels: RUNNER_LABELS,
271
+ registeredOrgs: succeeded,
272
+ filterUsed: filter.length > 0 ? filter : null,
273
+ }, home);
274
+
275
+ // 6. Summarize.
276
+ logger.log('');
277
+ logger.log(`────── Install summary ──────`);
278
+ logger.log(`Successfully registered: ${succeeded.length} / ${orgs.length} org(s)`);
279
+ if (succeeded.length > 0) logger.log(` ✓ ${succeeded.join(', ')}`);
280
+ if (failedByReason.size > 0) {
281
+ logger.log(`Failed: ${orgs.length - succeeded.length} org(s)`);
282
+ for (const [reason, failedOrgs] of failedByReason) {
283
+ logger.log(` ✗ ${reason}`);
284
+ logger.log(` affected: ${failedOrgs.join(', ')}`);
285
+ }
286
+ }
287
+ logger.log('');
288
+
289
+ if (succeeded.length === 0 && orgs.length > 0) {
290
+ logger.error(`Install failed: 0 of ${orgs.length} orgs registered. Fix the errors above, then run 'npx omega runner install' again.`);
291
+ process.exitCode = 1;
292
+ return;
293
+ }
294
+
295
+ logger.log('Install complete. Auto-restart at next logon is wired up via the Startup folder shortcut.');
296
+
297
+ // 7. Hand off to the runner: the same all-orgs, idempotent `start` the
298
+ // operator would run next, so an install ends with EVERY registered org
299
+ // online and the calling terminal free. No TTY gate on it any more: a
300
+ // detached spawn blocks nobody, so a scripted or scheduled install ends
301
+ // online too, instead of waiting for the next interactive logon.
302
+ if (succeeded.length > 0) {
303
+ logger.log('');
304
+ logger.log(`Bringing every registered runner (${succeeded.length}) online in the background. 'npx omega runner status' shows them, 'npx omega runner monitor' tails the log.`);
305
+ logger.log('');
306
+ await startServices({ ...options, _home: home });
307
+ }
308
+ }
309
+
310
+ // OMEGA_RUNNER_ORGS against the orgs the token actually administers: the ones
311
+ // it names (case-insensitively), plus the names no admin org answers to. A
312
+ // blank list is not a filter — it means every admin org, which is what a box
313
+ // nobody narrowed serves.
314
+ function selectRunnerOrgs(allAdminOrgs, filterRaw) {
315
+ const filter = parseRunnerOrgs(filterRaw);
316
+ if (filter.length === 0) return { orgs: allAdminOrgs, filter, unmatched: [], filtered: false };
317
+ const wanted = new Set(filter.map((o) => o.toLowerCase()));
318
+ return {
319
+ orgs: allAdminOrgs.filter((o) => wanted.has(o.toLowerCase())),
320
+ filter,
321
+ unmatched: filter.filter((f) => !allAdminOrgs.find((a) => a.toLowerCase() === f.toLowerCase())),
322
+ filtered: true,
323
+ };
324
+ }
325
+
326
+ // ─── config ─────────────────────────────────────────────────────────────────────
327
+ async function configureRunner(options) {
328
+ options = options || {};
329
+ ensureWindows();
330
+ const home = options._home || RUNNER_HOME;
331
+ const env = options._env || process.env;
332
+ const log = options._logger || logger;
333
+
334
+ const registered = readConfig(home).registeredOrgs || [];
335
+ const { file, orgs } = await reconfigureRunnerEnv({
336
+ home,
337
+ env,
338
+ logger: log,
339
+ prompt: options._prompt,
340
+ interactive: options._interactive,
341
+ discoverOrgs: options._discoverOrgs || discoverAdminOrgs,
342
+ registeredOrgs: registered,
343
+ requireTerminal: true,
344
+ });
345
+ log.log(`Saved to ${file}.`);
346
+
347
+ // config writes the list; only install acts on it.
348
+ if (orgs && !sameRunnerOrgs(orgs, registered)) {
349
+ log.log(`Run \`npx omega runner install\` to re-register against the new list.`);
350
+ }
351
+ }
352
+
353
+ // ─── register-org ───────────────────────────────────────────────────────────────
354
+ async function registerOrg(options) {
355
+ options = options || {};
356
+ ensureWindows();
357
+ const home = options._home || RUNNER_HOME;
358
+ await ensureRunnerConfig({ home, logger, scopes: ['runner'] });
359
+ ensureGhToken();
360
+
361
+ const org = options._?.[2] || options.org;
362
+ if (!org) throw new Error('Usage: npx omega runner register-org <org-name>');
363
+
364
+ const { getOctokit } = require('../utils/github.js');
365
+ const octokit = getOctokit();
366
+
367
+ // Delete any existing runners on the org side that match our naming convention for
368
+ // THIS host. Without this, re-running install accumulates orphaned runners (one per
369
+ // failed/aborted install, one per host rename, etc.) which causes actions/runner to
370
+ // auto-suffix the new runner's name (e.g. `-2872`) and breaks our ability to predict
371
+ // service names. Match prefix is `omega-runner-<host>-<org>` — and the electron-manager
372
+ // era's `em-runner-<host>-<org>`, the same box's older registration — so we never touch
373
+ // user-created runners or runners from other hosts.
374
+ //
375
+ // We do this BEFORE fetching the registration token because the token is one-shot
376
+ // and we want it fresh right before config.cmd uses it.
377
+ const hostPrefixes = [`omega-runner-`, LEGACY_RUNNER_PREFIX].map((p) => `${p}${os.hostname().toLowerCase()}-${org.toLowerCase()}`);
378
+ try {
379
+ const { data: existing } = await octokit.rest.actions.listSelfHostedRunnersForOrg({ org, per_page: 100 });
380
+ const ours = (existing.runners || []).filter((r) => hostPrefixes.some((p) => (r.name || '').toLowerCase().startsWith(p)));
381
+ for (const r of ours) {
382
+ try {
383
+ await octokit.rest.actions.deleteSelfHostedRunnerFromOrg({ org, runner_id: r.id });
384
+ logger.log(` Deleted stale runner ${r.name} (id=${r.id}) on ${org}`);
385
+ } catch (e) {
386
+ logger.warn(` Failed to delete stale runner ${r.name} (id=${r.id}): ${e.message}`);
387
+ }
388
+ }
389
+ } catch (e) {
390
+ // 403 here means GH_TOKEN can list but not delete — surface it but continue. Worst
391
+ // case: actions/runner auto-suffixes and the user gets a working but ugly-named runner.
392
+ logger.warn(` Could not list/delete existing runners for ${org}: ${e.message}`);
393
+ }
394
+
395
+ // Fetch a runner registration token (1-hour expiry, used immediately).
396
+ let regToken;
397
+ try {
398
+ const { data } = await octokit.rest.actions.createRegistrationTokenForOrg({ org });
399
+ regToken = data.token;
400
+ } catch (e) {
401
+ if (e.status === 403) {
402
+ throw new Error(`GH_TOKEN lacks admin:org scope for ${org}. Classic PATs need 'admin:org' (full) for runner registration. Re-issue at https://github.com/settings/tokens.`);
403
+ }
404
+ throw e;
405
+ }
406
+
407
+ // Per-org actions-runner directory. Each registration writes config files at the top of
408
+ // its dir; sharing one dir across orgs would race + clobber. Cost: ~120 MB per org on disk.
409
+ //
410
+ // Always nuke + re-clone from _template. Otherwise stale config (.runner, .credentials,
411
+ // _diag/) from a prior failed/partial run sticks around — and on the next install,
412
+ // config.cmd refuses with "Cannot configure the runner because it is already configured."
413
+ // Reusing the dir was an attempt at idempotency that broke re-install. Fresh dir per
414
+ // install is the only reliably-clean state.
415
+ const runnerDir = orgRunnerDir(org, home);
416
+ const templateDir = options._templateDir || path.join(home, '_template');
417
+ if (!jetpack.exists(path.join(templateDir, 'config.cmd'))) {
418
+ throw new Error(`actions/runner template not found at ${templateDir}. Run 'npx omega runner install' first.`);
419
+ }
420
+ if (jetpack.exists(runnerDir)) {
421
+ logger.log(` Removing stale actions-runner-${org.toLowerCase()}/ before re-clone…`);
422
+ jetpack.remove(runnerDir);
423
+ }
424
+ logger.log(` Cloning actions-runner template → actions-runner-${org.toLowerCase()}/`);
425
+ jetpack.copy(templateDir, runnerDir, { overwrite: true });
426
+
427
+ const configCmd = path.join(runnerDir, 'config.cmd');
428
+ const runnerName = ghRunnerName(org);
429
+
430
+ // config.cmd registers the runner against GH and writes .runner / .credentials
431
+ // into the per-org dir. We do NOT pass --runasservice — service-mode runners
432
+ // run in Session 0 (no desktop, no access to the user's CurrentUser\My cert
433
+ // store) and signtool can't see EV certs there. The Startup-folder shortcut
434
+ // written below is what brings the runner up, in the user's own session.
435
+ const args = [
436
+ '--unattended',
437
+ '--url', `https://github.com/${org}`,
438
+ '--token', regToken,
439
+ '--name', runnerName,
440
+ '--labels', RUNNER_LABELS.join(','),
441
+ '--replace',
442
+ ];
443
+
444
+ // Run via `cmd.exe /c` instead of `shell: true`. .cmd files can't be spawned directly
445
+ // by Node's CreateProcess on Windows, so we need cmd.exe as the shell — but `shell: true`
446
+ // triggers Node 24's DEP0190 deprecation warning when args are passed as an array.
447
+ // Explicit `cmd.exe /c <script> <args>` is the supported, warning-free path.
448
+ //
449
+ // stdio: 'inherit' — config.cmd's --runasservice service-install path silently
450
+ // SKIPS service creation when stdout/stderr are piped (Node captures them and the
451
+ // child sees no console). With inherit, the runner's banner + progress + service
452
+ // install messages stream directly to the user's terminal and the install actually
453
+ // happens. Discovered the hard way after several "registered but no service" rounds.
454
+ const { spawnSync } = require('child_process');
455
+ logger.log(`Registering against ${org} (cwd: ${runnerDir})…`);
456
+ const r = spawnSync('cmd.exe', ['/c', configCmd, ...args], {
457
+ cwd: runnerDir,
458
+ stdio: 'inherit',
459
+ timeout: 180000, // 3 min — config.cmd does network + service install
460
+ });
461
+
462
+ if (r.error) {
463
+ throw new Error(`Could not spawn config.cmd: ${r.error.message}`);
464
+ }
465
+ if (r.status !== 0) {
466
+ throw new Error(`config.cmd exited ${r.status === null ? 'null (killed)' : r.status} for ${org}. See output above.`);
467
+ }
468
+
469
+ // Verify config.cmd actually wrote the registration files. Without these the
470
+ // runner can't authenticate with GH at run time.
471
+ if (!jetpack.exists(path.join(runnerDir, '.runner')) || !jetpack.exists(path.join(runnerDir, '.credentials'))) {
472
+ throw new Error(`config.cmd succeeded but .runner / .credentials are missing in ${runnerDir}. The runner is not configured.`);
473
+ }
474
+
475
+ // The runner's private HOME ([#807](https://github.com/Omega-JS-Stack/omega/issues/807)):
476
+ // `actions/checkout` copies `$HOME\.gitconfig` into the temporary HOME it runs
477
+ // git under, and a SYMLINK source comes back as a Windows junction, which is
478
+ // invalid for a file. The box's `~/.gitconfig` is one, so the runner is
479
+ // pointed at a home of its own with a real file in it. Delivered through this
480
+ // runner's `<runner dir>\.env`, which the listener reads at startup and
481
+ // applies to every job, so the detached spawn and the Startup shortcut both
482
+ // get it from one write.
483
+ const privateHome = ensureRunnerPrivateHome(home, { logger });
484
+ ensureRunnerDirEnv(runnerDir, { HOME: privateHome.home });
485
+ logger.log(` HOME for ${org}: ${privateHome.home}`);
486
+
487
+ // Write the .cmd shortcut in the user's Startup folder so Explorer auto-runs
488
+ // it at every interactive logon (Session 1). No Task Scheduler, no admin
489
+ // needed for the trigger.
490
+ //
491
+ // We do NOT spawn the runner here. install() (when invoked interactively)
492
+ // starts every registered runner detached AFTER the registration loop
493
+ // completes. For non-interactive invocations (e.g. someone calling
494
+ // register-org directly from a script) the runner stays dormant until the
495
+ // next logon fires the Startup shortcut, or until `mgr runner start` is
496
+ // invoked.
497
+ writeRunnerStartupShortcut({ runnerName, runnerDir });
498
+
499
+ // Track in our config.
500
+ const cfg = readConfig(home);
501
+ cfg.registeredOrgs = Array.from(new Set([...(cfg.registeredOrgs || []), org]));
502
+ saveConfig(cfg, home);
503
+
504
+ logger.log(`✓ Registered runner '${runnerName}' for ${org} (auto-starts at logon via Startup folder)`);
505
+ }
506
+
507
+ // ─── start / stop / status ──────────────────────────────────────────────────────
508
+
509
+ // What `start` settles before it touches a listener: the box's required keys —
510
+ // asked in a terminal and refused without one, exactly like install — and then
511
+ // the orgs the .env names against the orgs this install actually registered.
512
+ // start registers nothing; it names the command that would.
513
+ async function startPreflight(options) {
514
+ const { home, env = process.env, logger: log = logger, prompt, interactive } = options || {};
515
+ await ensureRunnerConfig({ home, env, logger: log, prompt, interactive });
516
+
517
+ // Heal the private HOME ([#807](https://github.com/Omega-JS-Stack/omega/issues/807)):
518
+ // an install that predates it, or a runner dir whose `.env` was replaced, has
519
+ // no HOME line, and the job it picks up then dies inside `actions/checkout`.
520
+ // Every registered dir gets it here, before a single listener comes up.
521
+ const privateHome = ensureRunnerPrivateHome(home, { logger: log });
522
+ for (const { org, dir } of listOrgRunnerDirs(home)) {
523
+ ensureRunnerDirEnv(dir, { HOME: privateHome.home });
524
+ log.log(`HOME for ${org}: ${privateHome.home}`);
525
+ }
526
+
527
+ const named = parseRunnerOrgs(env.OMEGA_RUNNER_ORGS);
528
+ const registered = readConfig(home).registeredOrgs || [];
529
+ if (named.length > 0 && !sameRunnerOrgs(named, registered)) {
530
+ log.warn(`OMEGA_RUNNER_ORGS names ${named.join(', ')} but this install registered ${registered.join(', ') || '(none)'}. Run \`npx omega runner install\` to apply.`);
531
+ }
532
+ }
533
+
534
+ // The shortcuts a box has, each mapped back to its org and that org's runner
535
+ // dir. The shortcut naming convention is omega-runner-<host>-<org>; the org dir
536
+ // is actions-runner-<org>. Hostname can contain dashes (e.g. desktop-ifl07vg),
537
+ // so we strip a known host prefix rather than assume the host segment is
538
+ // dash-free. `start` walks these, and `restart` waits on the same list.
539
+ function runnerStartupTargets(home) {
540
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
541
+ return listRunnerStartupShortcuts().map((runnerName) => {
542
+ const orgName = runnerName.toLowerCase().startsWith(hostPrefix) ? runnerName.slice(hostPrefix.length) : null;
543
+ return { runnerName, orgName, runnerDir: orgName ? orgRunnerDir(orgName, home) : null };
544
+ });
545
+ }
546
+
547
+ // Every registered org, brought online detached, in Startup-shortcut order.
548
+ //
549
+ // A box registers one runner per org, and `start` used to bring up exactly ONE
550
+ // of them: the first shortcut, foregrounded in the calling terminal, with every
551
+ // other org left offline until the next logon
552
+ // ([#801](https://github.com/Omega-JS-Stack/omega/issues/801)). It is now the
553
+ // idempotent way to get the box online: an org whose listener is already alive
554
+ // is named and skipped (two listeners on one registration is the takeover
555
+ // storm), the rest are spawned detached, and the terminal stays free. The
556
+ // listener's own output is the JSONL log `monitor` tails, so nothing is lost by
557
+ // not foregrounding.
558
+ //
559
+ // `_spawn` and `_listeners` are injected by the tests, the same way `_home` and
560
+ // `_logger` are: neither cmd.exe nor the process scan exists off Windows.
561
+ // `_preflightDone` is `restart`'s: it settles the box config BEFORE it kills
562
+ // anything, so the walk must not run a second time here.
563
+ async function startServices(options) {
564
+ options = options || {};
565
+ ensureWindows();
566
+ const home = options._home || RUNNER_HOME;
567
+ const log = options._logger || logger;
568
+ const spawnIt = options._spawn || spawnRunnerDetached;
569
+ const listen = options._listeners || listRunnerListenerProcessesUnder;
570
+ if (!options._preflightDone) {
571
+ await startPreflight({ home, env: options._env || process.env, logger: log, prompt: options._prompt, interactive: options._interactive });
572
+ }
573
+ const targets = runnerStartupTargets(home);
574
+ if (targets.length === 0) {
575
+ log.warn('No omega-runner-* Startup shortcuts installed. Run `npx omega runner install` first.');
576
+ return;
577
+ }
578
+ let offline = 0;
579
+ for (const { runnerName, orgName, runnerDir } of targets) {
580
+ if (!runnerDir || !jetpack.exists(runnerDir)) {
581
+ log.warn(`✗ Could not resolve runner dir from shortcut name ${runnerName}`);
582
+ offline++;
583
+ continue;
584
+ }
585
+ // Already alive: one line naming the org and its PIDs, and on to the next.
586
+ // Not a refusal, and not a failure either, since that org IS online.
587
+ const existing = listen(runnerDir);
588
+ if (existing.length > 0) {
589
+ const detail = existing.map(({ pid, sessionId }) => `PID=${pid} session=${sessionId}`).join(', ');
590
+ // A session-0 listener is alive but useless: it fails every job it picks
591
+ // up. Say so loudly, and name the command that replaces it. `start` still
592
+ // skips it, because killing a listener is `stop`'s job and never a
593
+ // start's, and the org is not counted offline for it.
594
+ if (existing.some(({ sessionId }) => sessionId === 0)) {
595
+ log.warn(`⚠ ${orgName} already running in SESSION 0 (${detail}): ${SESSION_0_NOTE}. Run \`npx omega runner restart\` to replace it.`);
596
+ } else {
597
+ log.log(`· ${orgName} already running (${detail})`);
598
+ }
599
+ continue;
600
+ }
601
+ const r = spawnIt(runnerDir, home);
602
+ if (r.ok) {
603
+ log.log(`✓ start ${runnerName} (PID=${r.pid})`);
604
+ } else {
605
+ log.warn(`✗ start ${runnerName}: ${r.message}`);
606
+ offline++;
607
+ }
608
+ }
609
+ // The exit code a script or a logon task reads: every org online, or not.
610
+ if (offline > 0) process.exitCode = 1;
611
+ }
612
+
613
+ // stop, then start: the loop an operator ran by hand after every config change.
614
+ // The order is the point, so it is one command rather than two.
615
+ //
616
+ // Two things it settles that the two commands typed in sequence do not:
617
+ // 1. the box config walk runs FIRST, before a single listener is killed. Run
618
+ // inside `start`, a box missing a required key would be stopped and only
619
+ // then refused, which leaves it offline.
620
+ // 2. `taskkill` returns before the process is actually gone, so each org's dir
621
+ // is polled until no listener stands in it. Without that wait, `start`
622
+ // reads a dying listener as "already running" and calls a box that is on
623
+ // its way down online.
624
+ async function restartServices(options) {
625
+ options = options || {};
626
+ ensureWindows();
627
+ const home = options._home || RUNNER_HOME;
628
+ const log = options._logger || logger;
629
+ const listen = options._listeners || listRunnerListenerProcessesUnder;
630
+
631
+ await startPreflight({ home, env: options._env || process.env, logger: log, prompt: options._prompt, interactive: options._interactive });
632
+ await stopServices(options);
633
+
634
+ let standing = 0;
635
+ for (const { orgName, runnerDir } of runnerStartupTargets(home)) {
636
+ if (!runnerDir || !jetpack.exists(runnerDir)) continue;
637
+ const left = await waitForListenersGone({ runnerDir, listen, delay: options._delay });
638
+ if (left.length === 0) continue;
639
+ log.warn(`✗ ${orgName} still has a listener after stop (${left.map(({ pid }) => `PID=${pid}`).join(', ')}): not starting a second one against the same registration.`);
640
+ standing++;
641
+ }
642
+ if (standing > 0) process.exitCode = 1;
643
+
644
+ await startServices({ ...options, _preflightDone: true });
645
+ }
646
+
647
+ // How long `restart` waits for a killed listener to leave the process table:
648
+ // five seconds per org, polled. `delay` is the test's seam.
649
+ const STOP_POLL_ATTEMPTS = 20;
650
+ const STOP_POLL_INTERVAL_MS = 250;
651
+
652
+ async function waitForListenersGone(options) {
653
+ const { runnerDir, listen, delay, attempts = STOP_POLL_ATTEMPTS, intervalMs = STOP_POLL_INTERVAL_MS } = options || {};
654
+ const wait = delay || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
655
+ let left = listen(runnerDir);
656
+ for (let attempt = 0; left.length > 0 && attempt < attempts; attempt++) {
657
+ await wait(intervalMs);
658
+ left = listen(runnerDir);
659
+ }
660
+ return left;
661
+ }
662
+
663
+ async function stopServices(options) {
664
+ options = options || {};
665
+ ensureWindows();
666
+ const home = options._home || RUNNER_HOME;
667
+ const log = options._logger || logger;
668
+ // Stopping = killing all Runner.Listener.exe processes whose path lives
669
+ // under the runner home. /T also kills any Runner.Worker.exe children mid-job.
670
+ // Startup shortcuts are left in place so a logout/login still re-spawns;
671
+ // for a permanent stop, run `mgr runner uninstall`.
672
+ const procs = listRunnerListenerProcessesUnder(home);
673
+ if (procs.length === 0) {
674
+ log.log(`No Runner.Listener.exe processes under ${home} — already stopped.`);
675
+ } else {
676
+ const { spawnSync } = require('child_process');
677
+ for (const { pid, execPath } of procs) {
678
+ const k = spawnSync('taskkill', ['/F', '/PID', String(pid), '/T'], { encoding: 'utf8' });
679
+ if (k.status === 0) {
680
+ log.log(` ✓ Killed PID ${pid} (${execPath})`);
681
+ } else {
682
+ log.warn(` ✗ taskkill ${pid} (exit ${k.status}): ${(k.stderr || '').trim().slice(0, 200)}`);
683
+ }
684
+ }
685
+ }
686
+ }
687
+
688
+ async function statusServices(options) {
689
+ options = options || {};
690
+ ensureWindows();
691
+ const home = options._home || RUNNER_HOME;
692
+ const cfg = readConfig(home);
693
+ logger.log(`omega-runner home: ${home}`);
694
+ // The box's configuration — names only, never a value.
695
+ const envFile = runnerEnvFile(home);
696
+ // The names only, and from process.env — a passed `_home` names the FILE above
697
+ // but its values reached this process at require time, from RUNNER_HOME's.
698
+ const envKeys = runnerEnvReport();
699
+ logger.log(`Box config: ${envFile}${jetpack.exists(envFile) === 'file' ? '' : ' (missing — `npx omega runner install` writes the template)'}`);
700
+ logger.log(` set: ${envKeys.filter((k) => k.set).map((k) => k.key).join(', ') || '(none)'}`);
701
+ const unset = envKeys.filter((k) => !k.set).map((k) => k.key);
702
+ if (unset.length > 0) logger.warn(` unset: ${unset.join(', ')}`);
703
+ // The runner's private HOME and the real `.gitconfig` it exists for
704
+ // ([#807](https://github.com/Omega-JS-Stack/omega/issues/807)). A missing one
705
+ // is what a job's checkout dies on, so it is named here rather than guessed.
706
+ const privateHome = runnerPrivateHome(home);
707
+ const gitconfig = path.join(privateHome, '.gitconfig');
708
+ const hasGitconfig = jetpack.exists(gitconfig) === 'file';
709
+ logger.log(`Runner HOME: ${privateHome}`);
710
+ (hasGitconfig ? logger.log : logger.warn).call(logger, ` .gitconfig: ${hasGitconfig ? gitconfig : 'missing (`npx omega runner start` writes it)'}`);
711
+ logger.log(`Installed: ${cfg.installedAt || '(not yet)'}`);
712
+ logger.log(`Labels: ${(cfg.labels || RUNNER_LABELS).join(', ')}`);
713
+ logger.log(`Registered orgs: ${(cfg.registeredOrgs || []).join(', ') || '(none)'}`);
714
+ logger.log(`OMEGA_RUNNER_ORGS: ${parseRunnerOrgs(process.env.OMEGA_RUNNER_ORGS).join(', ') || '(blank — every admin org)'}`);
715
+ logger.log(`Log: ${runnerLogFile(home)}`);
716
+ logger.log('');
717
+
718
+ // Per-org Startup shortcuts (auto-start at every logon).
719
+ const shortcuts = listRunnerStartupShortcuts();
720
+ if (shortcuts.length === 0) {
721
+ logger.warn('No omega-runner-* Startup shortcuts installed.');
722
+ logger.warn('Run `npx omega runner install` to create them — registration alone is not enough.');
723
+ } else {
724
+ logger.log(`Runner Startup shortcuts (${shortcuts.length}):`);
725
+ for (const name of shortcuts) {
726
+ logger.log(` · ${name}.cmd → ${runnerStartupFile(name)}`);
727
+ }
728
+ }
729
+
730
+ // Per-org state — the SAME derivation and rendering `monitor` prints, so the
731
+ // two can never drift apart again.
732
+ logger.log('');
733
+ const report = orgRunnerReport({ home });
734
+ if (report.length === 0) {
735
+ logger.warn('No orgs registered. Run `npx omega runner install`.');
736
+ } else {
737
+ logger.log(`Per-org runner state (${report.length}):`);
738
+ for (const { state, lines } of report) {
739
+ const warn = state.sessionZero || state.state === 'NOT_INSTALLED';
740
+ for (const line of lines) (warn ? logger.warn : logger.log).call(logger, line);
741
+ }
742
+ if (report.some((r) => r.state.sessionZero)) {
743
+ logger.warn(` ⚠ Kill it ('npx omega runner stop') and re-spawn from Session 1, or it will fail every job it picks up.`);
744
+ }
745
+ if (report.every((r) => r.state.listeners.length === 0)) {
746
+ logger.warn('Run `npx omega runner start` to bring every registered runner online, or log out and back in.');
747
+ }
748
+ }
749
+
750
+ // Machine-wide safety net: a listener under the runner home that no registered
751
+ // org accounts for — a directory dropped from config.json, or a hand-started
752
+ // run.cmd. `stop` and `uninstall` both reach these; nothing else names them.
753
+ const accounted = new Set(report.flatMap((r) => r.state.listeners.map((p) => p.pid)));
754
+ const orphans = listRunnerListenerProcessesUnder(home).filter((p) => !accounted.has(p.pid));
755
+ if (orphans.length > 0) {
756
+ logger.log('');
757
+ logger.warn(`Unaccounted Runner.Listener.exe under the runner home (${orphans.length}) — no registered org claims them:`);
758
+ for (const { pid, sessionId, execPath } of orphans) {
759
+ logger.warn(` · PID=${pid} session=${sessionId} ${execPath || '(path unavailable)'}`);
760
+ }
761
+ }
762
+
763
+ // The electron-manager era's install (`em runner`): its home, its Startup
764
+ // shortcuts, and any listener still running out of it. None of it answers to
765
+ // the omega names above, so it is named here and torn down by `uninstall`.
766
+ const legacyHomes = listLegacyRunnerHomes({ home });
767
+ const legacyShortcuts = listLegacyRunnerStartupShortcuts();
768
+ if (legacyHomes.length > 0 || legacyShortcuts.length > 0) {
769
+ logger.log('');
770
+ logger.warn(`Legacy em-runner install detected — 'npx omega runner uninstall' (or 'install', which tears down first) deregisters and removes it:`);
771
+ for (const legacyHome of legacyHomes) {
772
+ const orgs = listOrgRunnerDirs(legacyHome).map((d) => d.org);
773
+ logger.warn(` · ${legacyHome}${orgs.length > 0 ? ` (orgs: ${orgs.join(', ')})` : ''}`);
774
+ for (const { pid, sessionId, execPath } of listRunnerListenerProcessesUnder(legacyHome)) {
775
+ logger.warn(` PID=${pid} session=${sessionId} ${execPath || '(path unavailable)'} — still picking up jobs`);
776
+ }
777
+ }
778
+ for (const name of legacyShortcuts) logger.warn(` · ${runnerStartupFile(name)}`);
779
+ }
780
+
781
+ // Surface leftover legacy services so users can clean them up.
782
+ const legacyServices = listActionsRunnerServices();
783
+ if (legacyServices.length > 0) {
784
+ logger.log('');
785
+ logger.warn(`Legacy actions.runner.* services detected (${legacyServices.length}). Run 'npx omega runner uninstall' to remove them.`);
786
+ for (const name of legacyServices) logger.log(` · ${name}`);
787
+ }
788
+
789
+ // Surface a leftover watcher service if one is still installed from older
790
+ // @omega.js/desktop versions — v1.2.35+ doesn't install it, but upgraders may have one.
791
+ const watcherState = scState(WATCHER_SERVICE_NAME);
792
+ if (watcherState !== 'NOT_INSTALLED') {
793
+ logger.log('');
794
+ logger.warn(`Legacy watcher service detected: ${WATCHER_SERVICE_NAME} (${watcherState}). v1.2.35+ doesn't use it. Run 'npx omega runner uninstall' to remove.`);
795
+ }
796
+ }
797
+
798
+ // ─── uninstall ──────────────────────────────────────────────────────────────────
799
+ async function uninstall(options) {
800
+ options = options || {};
801
+ ensureWindows();
802
+ const home = options._home || RUNNER_HOME;
803
+ // No admin needed. Per-user runner-home removal, killing your own
804
+ // Runner.Listener.exe processes, and deleting Startup-folder shortcuts all
805
+ // work without it. Legacy cleanup paths (watcher service, Logon Tasks) still
806
+ // need admin to delete fully — when not admin we attempt them anyway and
807
+ // simply log warnings on failure rather than blocking the uninstall.
808
+
809
+ // Delete each omega-runner-<host>-<org>.cmd file in the user's Startup folder
810
+ // so the runner doesn't auto-spawn at next logon. Scoped by STARTUP_DIR, which
811
+ // a test points at a scratch, so this one runs under a test like any other day.
812
+ const shortcuts = listRunnerStartupShortcuts();
813
+ for (const runnerName of shortcuts) {
814
+ const removed = removeRunnerStartupShortcut(runnerName);
815
+ if (removed) logger.log(` ✓ Removed Startup shortcut: ${runnerName}.cmd`);
816
+ }
817
+
818
+ // A test has no machine to sweep. These three are MACHINE-WIDE and take no
819
+ // path: an older install's watcher service, the v1.2.16–v1.2.34 logon tasks
820
+ // (which otherwise keep spawning Session 0 zombies after an upgrade), and the
821
+ // legacy `actions.runner.*` services from when runners registered as Windows
822
+ // services. None has a scratch twin the way `_home` and OMEGA_RUNNER_STARTUP_DIR
823
+ // give the home and the Startup folder one, so the only safe test behaviour is
824
+ // not touching them — said out loud, so a run that expected a sweep sees why
825
+ // it did not get one. All three are idempotent, so a real run re-runs clean.
826
+ if (isTestRun()) {
827
+ logger.log('Test run: skipping the machine-wide sweeps (services, logon tasks, legacy homes).');
828
+ } else {
829
+ await uninstallWatcherService();
830
+ await uninstallLegacyLogonTasks();
831
+ await uninstallActionsRunnerServices();
832
+ }
833
+
834
+ // Deregister every org-side runner, BEFORE the directories that hold the
835
+ // registration are removed. Anything that did not come off keeps its dir.
836
+ const { failed } = await deregisterOrgRunners({ home });
837
+
838
+ // Kill any Runner.Listener.exe processes whose path lives under the runner home
839
+ // BEFORE we try to remove the directory. These are usually leftovers from the
840
+ // legacy "double-click run.cmd" workflow (foreground runners not registered as
841
+ // a Scheduled Task or service) — uninstall otherwise has no way to know about
842
+ // them, and their open file handles inside the runner dirs will fail
843
+ // jetpack.remove with EPERM. Also catches Runner.Listener instances spawned by
844
+ // a currently-running Logon Task that `schtasks /End` (above) may have raced.
845
+ killRunnerListenerProcessesUnderHome(home);
846
+
847
+ // Now safe to remove disk state, minus any directory still holding a live
848
+ // registration. Retry a few times if files are locked (services release file
849
+ // handles asynchronously after stop).
850
+ await removeRunnerHomeWithRetry(new Set(failed.map((f) => f.dir)), home);
851
+
852
+ // The electron-manager era's install, when this box still has one: the same
853
+ // teardown, org by org, so its GitHub-side registrations come off too.
854
+ const legacy = await uninstallLegacyRunnerInstalls(home);
855
+ const stillRegistered = [...failed, ...legacy.failed];
856
+
857
+ if (stillRegistered.length === 0) {
858
+ logger.log('Uninstalled omega-runner.');
859
+ return;
860
+ }
861
+ logger.warn(`Uninstalled omega-runner, but ${stillRegistered.length} runner(s) are STILL registered on GitHub: ${stillRegistered.map((f) => f.org).join(', ')}`);
862
+ logger.warn(` Their directories were kept — re-run 'npx omega runner uninstall' to retry, or delete them under each org's Settings → Actions → Runners.`);
863
+ }
864
+
865
+ // Tear down the electron-manager era's runner install: its `em-runner-*`
866
+ // Startup shortcuts, then each legacy home — deregister every org dir through
867
+ // its own config.cmd, kill what runs out of it, remove it. A registration that
868
+ // did not come off keeps its directory, exactly as the omega home's does.
869
+ // Returns { failed: [{ org, dir, configCmd }] }.
870
+ async function uninstallLegacyRunnerInstalls(home) {
871
+ const failed = [];
872
+
873
+ for (const name of listLegacyRunnerStartupShortcuts()) {
874
+ if (removeRunnerStartupShortcut(name)) logger.log(` ✓ Removed legacy Startup shortcut: ${name}.cmd`);
875
+ }
876
+
877
+ // The legacy homes are machine-wide too — `C:\actions-runners` and
878
+ // `%LOCALAPPDATA%\em-runner`, neither of them a path this call can redirect —
879
+ // so a test stops here. The `em-runner-*` shortcut sweep above rides
880
+ // STARTUP_DIR, which a test DOES redirect, so it already ran.
881
+ if (isTestRun()) return { failed };
882
+
883
+ for (const legacyHome of listLegacyRunnerHomes({ home })) {
884
+ logger.log(`Legacy em-runner install at ${legacyHome} — tearing it down…`);
885
+ const result = await deregisterOrgRunners({ home: legacyHome });
886
+ killRunnerProcessesUnderHome(legacyHome);
887
+ await removeRunnerHomeWithRetry(new Set(result.failed.map((f) => f.dir)), legacyHome);
888
+ failed.push(...result.failed);
889
+ }
890
+
891
+ return { failed };
892
+ }
893
+
894
+ async function uninstallWatcherService() {
895
+ // Quick check: does the service exist? Avoid noisy 1060 errors on a clean uninstall.
896
+ const exists = await scQueryExists(WATCHER_SERVICE_NAME);
897
+ if (!exists) return;
898
+
899
+ const { spawnSync } = require('child_process');
900
+
901
+ // CRITICAL: clear the failure-action config FIRST. node-windows configures
902
+ // the watcher with restart-on-failure, so a plain `sc stop` triggers SCM
903
+ // to immediately respawn it, defeating the uninstall. We blank the restart
904
+ // policy first, THEN stop, THEN kill any stragglers, THEN delete.
905
+ // (We used to use node-windows' own svc.uninstall(), but it left the
906
+ // service running in v1.2.34, kept spawning rogue Runner.Listener.exe in
907
+ // Session 0, and the recursive uninstall called from install() then hit
908
+ // EPERM trying to remove RUNNER_HOME.)
909
+ spawnSync('sc', ['failure', WATCHER_SERVICE_NAME, 'reset=', '0', 'actions='], { stdio: 'ignore' });
910
+ spawnSync('sc', ['stop', WATCHER_SERVICE_NAME], { stdio: 'ignore' });
911
+
912
+ // The watcher's wrapper executable is `emrunnerwatcher.exe` (node-windows
913
+ // names the daemon after the service). Force-kill any process by that
914
+ // name + any node.exe child whose CommandLine references watcher.js, in
915
+ // case SCM's stop didn't fully take.
916
+ spawnSync('taskkill', ['/F', '/IM', 'emrunnerwatcher.exe', '/T'], { stdio: 'ignore' });
917
+ const r = spawnSync('powershell', [
918
+ '-NoProfile', '-NonInteractive', '-Command',
919
+ `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*watcher.js*' } | ForEach-Object { $_.ProcessId }`,
920
+ ], { encoding: 'utf8' });
921
+ if (r.status === 0) {
922
+ for (const line of (r.stdout || '').split(/\r?\n/)) {
923
+ const pid = line.trim();
924
+ if (/^\d+$/.test(pid)) {
925
+ spawnSync('taskkill', ['/F', '/PID', pid, '/T'], { stdio: 'ignore' });
926
+ }
927
+ }
928
+ }
929
+
930
+ // Now safe to delete the service definition.
931
+ spawnSync('sc', ['delete', WATCHER_SERVICE_NAME], { stdio: 'ignore' });
932
+ logger.log(` ✓ Removed legacy watcher service: ${WATCHER_SERVICE_NAME}`);
933
+ }
934
+
935
+ async function uninstallActionsRunnerServices() {
936
+ const { spawnSync } = require('child_process');
937
+ // sc query state= all returns all services; grep for "actions.runner.".
938
+ const r = spawnSync('sc', ['query', 'state=', 'all'], { encoding: 'utf8' });
939
+ if (r.status !== 0) return;
940
+ const names = (r.stdout || '').match(/SERVICE_NAME:\s*(actions\.runner\.\S+)/g) || [];
941
+ for (const n of names) {
942
+ const name = n.replace(/SERVICE_NAME:\s*/, '').trim();
943
+ logger.log(`Removing leftover service ${name}…`);
944
+ spawnSync('sc', ['stop', name], { stdio: 'inherit' });
945
+ spawnSync('sc', ['delete', name], { stdio: 'inherit' });
946
+ }
947
+ }
948
+
949
+ async function scQueryExists(serviceName) {
950
+ const { spawnSync } = require('child_process');
951
+ const r = spawnSync('sc', ['query', serviceName], { encoding: 'utf8' });
952
+ return r.status === 0;
953
+ }
954
+
955
+ // Remove the runner home, except any directory in `keepDirs` — those still hold
956
+ // a registration that came off GitHub, and a later uninstall needs their
957
+ // config.cmd to retry with. With nothing to keep this removes the home whole,
958
+ // which is also the only way the template and the install record go.
959
+ async function removeRunnerHomeWithRetry(keepDirs, home) {
960
+ const root = home || RUNNER_HOME;
961
+ if (jetpack.exists(root) !== 'dir') return;
962
+
963
+ // The box's own .env is configuration, not install state: it survives every
964
+ // uninstall, so a re-install never asks for the PIN or the token again. So
965
+ // does its logs dir — runner.log is the trail of this very uninstall, still
966
+ // open by the tee while the removal runs.
967
+ const envFile = runnerEnvFile(root);
968
+ const logsDir = path.dirname(runnerLogFile(root));
969
+ const keep = new Set([...(keepDirs || []), envFile, logsDir]);
970
+ const targets = (jetpack.list(root) || [])
971
+ .map((name) => path.join(root, name))
972
+ .filter((entry) => !keep.has(entry));
973
+
974
+ // Nothing kept survives on disk: the home itself goes too.
975
+ const kept = [...(keepDirs || [])].length > 0
976
+ || jetpack.exists(envFile) === 'file'
977
+ || jetpack.exists(logsDir) === 'dir';
978
+ if (!kept) targets.push(root);
979
+
980
+ for (const target of targets) {
981
+ // Services release file handles asynchronously — give them a few seconds.
982
+ for (let i = 0; i < 5; i++) {
983
+ try {
984
+ jetpack.remove(target);
985
+ break;
986
+ } catch (e) {
987
+ if (i === 4) {
988
+ logger.warn(`Could not fully remove ${target} after 5 attempts: ${e.message}`);
989
+ // Try to name the offending process. Sysinternals handle.exe (if on PATH)
990
+ // gives us the holder. Otherwise we tell the user how to install it for
991
+ // next time, so failures here don't keep being "files may be locked" with
992
+ // no actionable info.
993
+ identifyHandleHolders(target);
994
+ break;
995
+ }
996
+ await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+
1002
+ // Force-kill every process whose execution path lives under RUNNER_HOME, with
1003
+ // `taskkill /F /T` so each process's children die too. Critical for uninstall:
1004
+ // without it, the cmd.exe wrapper that runs run.cmd holds the runner dir as
1005
+ // cwd for the lifetime of the listener, and jetpack.remove(RUNNER_HOME) fails
1006
+ // with EPERM. Killing only Runner.Listener.exe wasn't enough — `/T` kills its
1007
+ // children but NOT its parent cmd.exe, so the wrapper survives + keeps the
1008
+ // cwd lock. v1.2.37: we now also enumerate cmd.exe wrappers (CommandLine
1009
+ // references run.cmd from RUNNER_HOME) so the entire process tree is killed
1010
+ // in one pass.
1011
+ //
1012
+ // Path-unavailable Runner.Listener.exe instances (ExecutablePath comes back
1013
+ // empty when the process belongs to another account, even from an elevated
1014
+ // query) are killed too — uninstall should leave no listener behind. Except
1015
+ // under a test run: a test owns no such process (listenerFilterScript).
1016
+ function killRunnerProcessesUnderHome(home) {
1017
+ if (process.platform !== 'win32') return;
1018
+ const { spawnSync } = require('child_process');
1019
+
1020
+ // Single PowerShell call gets both Runner.Listener.exe AND cmd.exe wrappers
1021
+ // (any cmd.exe whose CommandLine references run.cmd under the home).
1022
+ // PID|ExecutablePath|Name|MatchedAs — '|' separators, easy to parse.
1023
+ const homeLower = String(home || RUNNER_HOME).toLowerCase().replace(/'/g, "''");
1024
+ // A test run drops the unreadable-path arm: that arm matches whatever home it
1025
+ // is given, so under a test it is the ONE clause in here that is not scoped by
1026
+ // anything (see listenerFilterScript).
1027
+ const ps = listenerFilterScript(homeLower, { includeUnreadable: !isTestRun() });
1028
+ const r = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], { encoding: 'utf8' });
1029
+ if (r.status !== 0) return;
1030
+
1031
+ const targets = [];
1032
+ for (const rawLine of (r.stdout || '').split(/\r?\n/)) {
1033
+ const line = rawLine.trim();
1034
+ if (!line) continue;
1035
+ const parts = line.split('|');
1036
+ if (parts.length < 4) continue;
1037
+ const [pid, execPath, name, tag] = parts;
1038
+ if (!/^\d+$/.test(pid)) continue;
1039
+ targets.push({ pid, execPath: execPath || '(path unavailable)', name, tag });
1040
+ }
1041
+
1042
+ if (targets.length === 0) return;
1043
+
1044
+ logger.log(`Killing ${targets.length} runner-related process(es) before disk cleanup…`);
1045
+ for (const { pid, execPath, name, tag } of targets) {
1046
+ const k = spawnSync('taskkill', ['/F', '/PID', pid, '/T'], { encoding: 'utf8' });
1047
+ if (k.status === 0) {
1048
+ logger.log(` ✓ Killed PID ${pid} (${name} ${tag}) ${execPath}`);
1049
+ } else {
1050
+ // Non-fatal: process may already be dead from a /T tree-kill of a
1051
+ // sibling target. Only warn on truly unexpected codes.
1052
+ const msg = (k.stderr || '').trim().slice(0, 200);
1053
+ if (!/not found|There are no/.test(msg)) {
1054
+ logger.warn(` ✗ taskkill ${pid} failed (exit ${k.status}): ${msg}`);
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ // The PowerShell `killRunnerProcessesUnderHome` runs, built apart from the spawn
1061
+ // so the one clause that decides what a test may kill is assertable off Windows.
1062
+ // `homeLc` arrives lowercased and PowerShell-escaped.
1063
+ //
1064
+ // `includeUnreadable` adds the `$ep -eq ''` arm. A Runner.Listener/Worker whose
1065
+ // ExecutablePath the query cannot read belongs to ANOTHER account, and that arm
1066
+ // matches it against WHATEVER home is passed — on a real box it is wanted (an
1067
+ // uninstall must clear every handle inside the home before it removes it), but a
1068
+ // test owns no such process, so a test run would be `taskkill /F`-ing a
1069
+ // stranger's runner with every home and Startup dir it holds pointed at a
1070
+ // scratch. Same rule as listRunnerListenerProcessesUnder's `!execPath` clause.
1071
+ function listenerFilterScript(homeLc, options) {
1072
+ const { includeUnreadable } = options || {};
1073
+ const listenerMatch = includeUnreadable
1074
+ ? `$ep.StartsWith($home_lc) -or $ep -eq ''`
1075
+ : `$ep.StartsWith($home_lc)`;
1076
+
1077
+ return `
1078
+ $home_lc = '${homeLc}';
1079
+ Get-CimInstance Win32_Process -Filter "Name='Runner.Listener.exe' OR Name='cmd.exe' OR Name='Runner.Worker.exe'" | ForEach-Object {
1080
+ $name = $_.Name;
1081
+ $cl = if ($_.CommandLine) { $_.CommandLine.ToLower() } else { '' };
1082
+ $ep = if ($_.ExecutablePath) { $_.ExecutablePath.ToLower() } else { '' };
1083
+ $matched = $false; $tag = '';
1084
+ if ($name -eq 'Runner.Listener.exe' -or $name -eq 'Runner.Worker.exe') {
1085
+ if (${listenerMatch}) { $matched = $true; $tag = 'listener' }
1086
+ } elseif ($name -eq 'cmd.exe') {
1087
+ if ($cl -like ('*' + $home_lc + '*') -and $cl -like '*run.cmd*') { $matched = $true; $tag = 'wrapper' }
1088
+ }
1089
+ if ($matched) { "$($_.ProcessId)|$($_.ExecutablePath)|$($_.Name)|$tag" }
1090
+ }
1091
+ `;
1092
+ }
1093
+
1094
+ // Backwards-compatible alias — old name used in uninstall code paths.
1095
+ const killRunnerListenerProcessesUnderHome = killRunnerProcessesUnderHome;
1096
+
1097
+ // Identify the processes currently holding handles inside `targetPath` via
1098
+ // Sysinternals handle.exe. Used as a diagnostic when removeRunnerHomeWithRetry
1099
+ // gives up — turns "files may be locked" into "PID 1234 (Runner.Listener.exe)
1100
+ // is holding <runner home>\actions-runner-foo\bin\Runner.Listener.exe".
1101
+ // handle.exe isn't bundled with Windows; if it isn't on PATH we surface a tip
1102
+ // instead so the next failure has a path to actionable info.
1103
+ function identifyHandleHolders(targetPath) {
1104
+ if (process.platform !== 'win32') return;
1105
+ const { spawnSync } = require('child_process');
1106
+
1107
+ const probe = spawnSync('where', ['handle.exe'], { stdio: ['ignore', 'pipe', 'ignore'] });
1108
+ if (probe.status !== 0) {
1109
+ logger.warn(`Tip: install Sysinternals handle.exe (https://learn.microsoft.com/sysinternals/downloads/handle) and add it to PATH so future failures here can name the offending process.`);
1110
+ logger.warn(`Some files may be locked. Reboot Windows and re-run install if this persists.`);
1111
+ return;
1112
+ }
1113
+
1114
+ // -accepteula bypasses the one-time EULA prompt that handle.exe shows on
1115
+ // first run; -nobanner suppresses the version banner so the output is
1116
+ // straight to per-handle lines.
1117
+ const r = spawnSync('handle.exe', ['-accepteula', '-nobanner', targetPath], { encoding: 'utf8' });
1118
+ const out = (r.stdout || '').trim();
1119
+ if (out) {
1120
+ logger.warn(`Processes holding handles under ${targetPath}:`);
1121
+ for (const line of out.split(/\r?\n/).slice(0, 30)) logger.warn(` ${line}`);
1122
+ } else {
1123
+ logger.warn(`handle.exe ran but reported no holders for ${targetPath}. The lock may be at the directory level (e.g. another shell's cwd is set inside it) — try closing all cmd windows and re-running.`);
1124
+ }
1125
+ }
1126
+
1127
+ // ─── self-update ────────────────────────────────────────────────────────────────
1128
+ async function selfUpdate() {
1129
+ const { execute } = require('node-powertools');
1130
+ logger.log('Updating @omega.js/desktop to latest…');
1131
+ try {
1132
+ const out = await safeInstall('npm i -g @omega.js/desktop@latest');
1133
+ logger.log(out);
1134
+ logger.log('✓ @omega.js/desktop updated.');
1135
+ } catch (e) {
1136
+ logger.warn(`Self-update failed: ${e.message}`);
1137
+ }
1138
+ }
1139
+
1140
+ // ─── helpers ────────────────────────────────────────────────────────────────────
1141
+
1142
+ const WATCHER_SERVICE_NAME = 'omega-runner-watcher';
1143
+
1144
+ function ensureWindows() {
1145
+ if (process.platform !== 'win32' && !process.env.OMEGA_RUNNER_FORCE) {
1146
+ throw new Error('This command only runs on Windows. Set OMEGA_RUNNER_FORCE=1 to override (testing only).');
1147
+ }
1148
+ }
1149
+
1150
+ function ensureGhToken() {
1151
+ if (!process.env.GH_TOKEN) {
1152
+ throw new Error('GH_TOKEN env var required. Classic PAT needs scopes: repo + workflow + admin:org. Set it before running runner commands.');
1153
+ }
1154
+ }
1155
+
1156
+ async function downloadActionsRunner(runnerDir, home) {
1157
+ const url = `https://github.com/actions/runner/releases/download/v${ACTIONS_RUNNER_VERSION}/actions-runner-win-x64-${ACTIONS_RUNNER_VERSION}.zip`;
1158
+ const zipPath = path.join(home || RUNNER_HOME, 'actions-runner.zip');
1159
+ jetpack.dir(runnerDir);
1160
+ logger.log(`Downloading actions/runner v${ACTIONS_RUNNER_VERSION}…`);
1161
+
1162
+ // curl ships on Windows 10+ and macOS. -L follows GitHub's redirect to the S3 download URL,
1163
+ // -f fails on HTTP errors (so we don't write an HTML error page as "the zip"), -o writes
1164
+ // to disk directly (no in-memory buffering, no truncation issues with large files).
1165
+ const { spawnSync } = require('child_process');
1166
+ const dl = spawnSync('curl', ['-fL', '-o', zipPath, url], { stdio: 'inherit' });
1167
+ if (dl.status !== 0) {
1168
+ throw new Error(`Failed to download actions-runner.zip (curl exit ${dl.status}). Check network or curl.exe availability.`);
1169
+ }
1170
+
1171
+ // Sanity-check size before extracting — actions/runner zip is ~150 MB. Anything under 1 MB
1172
+ // is almost certainly an error page that slipped through.
1173
+ const stat = fs.statSync(zipPath);
1174
+ if (stat.size < 1024 * 1024) {
1175
+ const head = fs.readFileSync(zipPath, 'utf8').slice(0, 200);
1176
+ throw new Error(`Downloaded actions-runner.zip is only ${stat.size} bytes — likely an error page. First 200 bytes: ${head}`);
1177
+ }
1178
+ logger.log(`Downloaded ${(stat.size / 1024 / 1024).toFixed(1)} MB → extracting…`);
1179
+
1180
+ // Extract via PowerShell's Expand-Archive instead of `tar`. On Windows the
1181
+ // System32 tar.exe (bsdtar) handles `C:\...` paths fine, but if the user's
1182
+ // PATH front-loads Git for Windows' tar (GNU tar), it interprets `C:\...`
1183
+ // as `host:path` and fails with "Cannot connect to C: resolve failed".
1184
+ // Expand-Archive is built into PowerShell 5.1+ on every supported Windows
1185
+ // and has none of those quirks.
1186
+ if (process.platform === 'win32') {
1187
+ const ps = spawnSync('powershell', [
1188
+ '-NoProfile',
1189
+ '-NonInteractive',
1190
+ '-Command',
1191
+ `Expand-Archive -Path '${zipPath}' -DestinationPath '${runnerDir}' -Force`,
1192
+ ], { stdio: 'inherit' });
1193
+ if (ps.status !== 0) {
1194
+ throw new Error(`Failed to extract actions-runner.zip via PowerShell Expand-Archive (exit ${ps.status}).`);
1195
+ }
1196
+ } else {
1197
+ const t = spawnSync('tar', ['-xf', zipPath, '-C', runnerDir], { stdio: 'inherit' });
1198
+ if (t.status !== 0) {
1199
+ throw new Error(`Failed to extract actions-runner.zip (tar exit ${t.status}).`);
1200
+ }
1201
+ }
1202
+ jetpack.remove(zipPath);
1203
+ logger.log(`Extracted actions/runner → ${runnerDir}`);
1204
+ }
1205
+
1206
+ async function discoverAdminOrgs() {
1207
+ const { getOctokit } = require('../utils/github.js');
1208
+ const octokit = getOctokit();
1209
+ if (!octokit) return [];
1210
+ const orgs = [];
1211
+ try {
1212
+ const { data } = await octokit.rest.orgs.listForAuthenticatedUser({ per_page: 100 });
1213
+ for (const org of data) {
1214
+ try {
1215
+ // membership endpoint shows our role.
1216
+ const { data: m } = await octokit.rest.orgs.getMembershipForAuthenticatedUser({ org: org.login });
1217
+ if (m.role === 'admin') orgs.push(org.login);
1218
+ } catch (e) { /* skip if can't read */ }
1219
+ }
1220
+ } catch (e) {
1221
+ logger.warn(`Could not list orgs: ${e.message}`);
1222
+ }
1223
+ return orgs;
1224
+ }
1225
+
1226
+ // Enumerate all `actions.runner.*` services on this machine (one per registered org).
1227
+ // Uses `sc query state= all` (note the space — that's the documented form) to get every
1228
+ // service including stopped ones, then filters by SERVICE_NAME prefix.
1229
+ function listActionsRunnerServices() {
1230
+ if (process.platform !== 'win32') return [];
1231
+ const { spawnSync } = require('child_process');
1232
+ const r = spawnSync('sc', ['query', 'state=', 'all'], { encoding: 'utf8' });
1233
+ if (r.status !== 0) return [];
1234
+ const out = r.stdout || '';
1235
+ const names = [];
1236
+ for (const line of out.split(/\r?\n/)) {
1237
+ const m = /^SERVICE_NAME:\s*(actions\.runner\..+)$/.exec(line.trim());
1238
+ if (m) names.push(m[1]);
1239
+ }
1240
+ return names.sort();
1241
+ }
1242
+
1243
+ // Parse the current STATE from `sc query <name>`. Returns 'RUNNING' | 'STOPPED' | 'NOT_INSTALLED' | 'UNKNOWN'.
1244
+ function scState(name) {
1245
+ if (process.platform !== 'win32') return 'UNKNOWN';
1246
+ const { spawnSync } = require('child_process');
1247
+ const r = spawnSync('sc', ['query', name], { encoding: 'utf8' });
1248
+ if (r.status === 1060) return 'NOT_INSTALLED';
1249
+ if (r.status !== 0) return 'UNKNOWN';
1250
+ const out = r.stdout || '';
1251
+ const m = /STATE\s*:\s*\d+\s+(\w+)/.exec(out);
1252
+ return m ? m[1].toUpperCase() : 'UNKNOWN';
1253
+ }
1254
+
1255
+ // ─── Startup folder runner management ─────────────────────────────────────
1256
+ //
1257
+ // The per-org runner auto-starts from a .cmd file in the user's Startup folder,
1258
+ // never a Scheduled Task. Reason: Task Scheduler runs ONLOGON tasks in its own
1259
+ // (Session 0) context regardless of the /IT flag, leaving the runner blind to
1260
+ // the user's CurrentUser\My cert store and unable to host the SafeNet Token
1261
+ // Logon PIN dialog. Files in the Startup folder are auto-run by Explorer at
1262
+ // every interactive logon (Session 1), no Task Scheduler middleman, no admin
1263
+ // needed for the auto-start trigger.
1264
+ //
1265
+ // File layout:
1266
+ // %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\<runnerName>.cmd
1267
+ //
1268
+ // Content:
1269
+ // @echo off
1270
+ // start "" /min /D "%LOCALAPPDATA%\omega-runner\actions-runner-<org>" "…\run.cmd"
1271
+ //
1272
+ // `start "" /min` launches run.cmd in a minimized window and returns
1273
+ // immediately, so subsequent startup items aren't blocked. The empty `""` is
1274
+ // start.exe's title parameter — required when the next argument is quoted,
1275
+ // otherwise start interprets the quoted path as a console title.
1276
+
1277
+ //
1278
+ // `OMEGA_RUNNER_STARTUP_DIR` is the Startup folder's scratch seam, parity with
1279
+ // `OMEGA_RUNNER_HOME` on the runner home (utils/runner-env.js `defaultRunnerHome`):
1280
+ // resolved ONCE here, at require time, so a test child sets it before it requires
1281
+ // this module. It exists because no home scopes this folder — `uninstall` sweeps
1282
+ // every `omega-runner-*.cmd` in it, and on 2026-09-04 a suite case with two
1283
+ // scratch homes deleted the signing box's three real shortcuts.
1284
+ const STARTUP_DIR = process.env.OMEGA_RUNNER_STARTUP_DIR
1285
+ || (process.platform === 'win32'
1286
+ ? path.join(
1287
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
1288
+ 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup',
1289
+ )
1290
+ : null);
1291
+
1292
+ function runnerStartupFile(runnerName, startupDir) {
1293
+ const dir = startupDir === undefined ? STARTUP_DIR : startupDir;
1294
+ if (!dir) return null;
1295
+ return path.join(dir, `${runnerName}.cmd`);
1296
+ }
1297
+
1298
+ // The state of ONE org's runner, from the three things this design actually
1299
+ // has: the per-org runner dir on disk, its Startup shortcut, and any live
1300
+ // Runner.Listener.exe underneath it. `status` prints these machine-wide;
1301
+ // `monitor` prints them per org. There is no Scheduled Task to ask
1302
+ // ([#337](https://github.com/Omega-JS-Stack/omega/issues/337)).
1303
+ function orgRunnerState(org, options) {
1304
+ const { home, startupDir, listListeners } = options || {};
1305
+ const name = ghRunnerName(org);
1306
+ const dir = orgRunnerDir(org, home);
1307
+ const shortcut = runnerStartupFile(name, startupDir);
1308
+ const listeners = (listListeners || listRunnerListenerProcessesUnder)(dir);
1309
+ const installed = jetpack.exists(dir) === 'dir';
1310
+
1311
+ // A listener in Session 0 is WORSE than a stopped one: it picks jobs up and
1312
+ // then fails every signtool call, because Session 0 has no desktop for the
1313
+ // Token Logon dialog and its own empty certificate store.
1314
+ const sessionZero = listeners.some((p) => p.sessionId === 0);
1315
+
1316
+ return {
1317
+ org,
1318
+ name,
1319
+ dir,
1320
+ shortcut,
1321
+ installed,
1322
+ autoStart: !!shortcut && jetpack.exists(shortcut) === 'file',
1323
+ listeners,
1324
+ sessionZero,
1325
+ state: listeners.length === 0
1326
+ ? (installed ? 'STOPPED' : 'NOT_INSTALLED')
1327
+ : (sessionZero ? 'SESSION_0' : 'RUNNING'),
1328
+ };
1329
+ }
1330
+
1331
+ const RUNNER_STATE_SYMBOLS = {
1332
+ RUNNING: '✓',
1333
+ STOPPED: '·',
1334
+ SESSION_0: '⚠',
1335
+ NOT_INSTALLED: '?',
1336
+ };
1337
+ const SESSION_0_NOTE = 'session 0: cannot see CurrentUser\\My cert store';
1338
+
1339
+ // The per-org block. `status` and `monitor` both print THIS — one derivation and
1340
+ // one rendering, so the two can never disagree about what a runner is doing
1341
+ // ([#337](https://github.com/Omega-JS-Stack/omega/issues/337): monitor used to
1342
+ // ask a pair of Scheduled-Task helpers that did not exist while status scanned
1343
+ // processes itself).
1344
+ function orgRunnerStateLines(state) {
1345
+ const notes = [
1346
+ state.installed ? `dir ${state.dir}` : `no runner dir at ${state.dir}`,
1347
+ state.autoStart ? 'auto-starts at logon' : 'no Startup shortcut',
1348
+ ];
1349
+ const lines = [` ${RUNNER_STATE_SYMBOLS[state.state] || '?'} ${state.org} — ${state.state} (${notes.join('; ')})`];
1350
+
1351
+ for (const { pid, sessionId, execPath } of state.listeners) {
1352
+ const where = execPath || '(path unavailable)';
1353
+ const suffix = sessionId === 0 ? ` — ${SESSION_0_NOTE}` : '';
1354
+ lines.push(` PID=${pid} session=${sessionId} ${where}${suffix}`);
1355
+ }
1356
+
1357
+ return lines;
1358
+ }
1359
+
1360
+ // Every registered org's state plus the lines that render it.
1361
+ function orgRunnerReport(options) {
1362
+ return monitorOrgStates(options).map((state) => ({ state, lines: orgRunnerStateLines(state) }));
1363
+ }
1364
+
1365
+ // The org roster `monitor` prints on start, one state per org.
1366
+ //
1367
+ // OMEGA_RUNNER_ORGS wins over config.registeredOrgs when set, because installs
1368
+ // that predated the filter often left a stale full-org list in config.json.
1369
+ function monitorOrgStates(options) {
1370
+ const { home, startupDir, env, listListeners } = options || {};
1371
+ const cfg = readConfig(home);
1372
+ let orgs = cfg.registeredOrgs || [];
1373
+
1374
+ const filter = parseRunnerOrgs((env || process.env).OMEGA_RUNNER_ORGS);
1375
+ if (filter.length > 0) {
1376
+ const filterSet = new Set(filter.map((o) => o.toLowerCase()));
1377
+ orgs = orgs.filter((o) => filterSet.has(o.toLowerCase()));
1378
+ if (orgs.length === 0) orgs = filter; // filter set but nothing matched in config — show the filter directly
1379
+ }
1380
+
1381
+ return orgs.map((org) => orgRunnerState(org, { home, startupDir, listListeners }));
1382
+ }
1383
+
1384
+ // Every per-org runner directory currently on disk that still holds the
1385
+ // `config.cmd` needed to deregister it.
1386
+ //
1387
+ // The roster comes off DISK, not config.json: a half-finished install leaves
1388
+ // directories config.json never recorded, and those are exactly the ones that
1389
+ // strand an orphaned runner in the org. `_template` is excluded by the name
1390
+ // filter — it carries a config.cmd but was never registered against anything.
1391
+ function listOrgRunnerDirs(home) {
1392
+ const root = home || RUNNER_HOME;
1393
+ if (jetpack.exists(root) !== 'dir') return [];
1394
+
1395
+ return (jetpack.list(root) || [])
1396
+ .filter((name) => /^actions-runner-.+$/i.test(name))
1397
+ .map((name) => name.replace(/^actions-runner-/i, ''))
1398
+ .map((org) => ({ org, dir: orgRunnerDir(org, root), configCmd: path.join(orgRunnerDir(org, root), 'config.cmd') }))
1399
+ .filter((entry) => jetpack.exists(entry.configCmd) === 'file')
1400
+ .sort((a, b) => a.org.localeCompare(b.org));
1401
+ }
1402
+
1403
+ // Remove each registration from the GitHub side, using that directory's OWN
1404
+ // config.cmd. Without this, every uninstall leaves an offline runner in the org
1405
+ // list until someone deletes it by hand.
1406
+ //
1407
+ // A removal that did NOT succeed keeps its directory: the registration files in
1408
+ // there are the only thing a later `uninstall` can retry with, so deleting them
1409
+ // would strand that runner in the org permanently.
1410
+ //
1411
+ // `exec` and `getRemoveToken` are injected by the tests — config.cmd only runs
1412
+ // on Windows, but the roster, the verdicts and the no-token skip are provable
1413
+ // anywhere. Returns { deregistered: [org], failed: [{ org, dir, configCmd }] }.
1414
+ async function deregisterOrgRunners(options) {
1415
+ const { home, exec, getRemoveToken } = options || {};
1416
+ const targets = listOrgRunnerDirs(home);
1417
+ if (targets.length === 0) return { deregistered: [], failed: [] };
1418
+
1419
+ // Neither default below is scoped by a home: `getRemoveToken` mints a REAL
1420
+ // removal token for the named org with GH_TOKEN, and `run` executes that
1421
+ // directory's own config.cmd. A test with a roster and no seam is therefore
1422
+ // one request away from deregistering the box's runners, which is what the
1423
+ // home and Startup-folder guards cannot see. Refuse instead of reaching.
1424
+ // `exec` alone is not enough while GH_TOKEN is set: the default
1425
+ // `getRemoveToken` would still mint a real removal token against the org.
1426
+ if (isTestRun() && (!exec || (!getRemoveToken && process.env.GH_TOKEN))) {
1427
+ throw new Error(
1428
+ `Refusing to deregister ${targets.length} runner(s) from GitHub: this is a test run `
1429
+ + '(OMEGA_TEST_RUNNER / OMEGA_TEST_MODE) and the call did not inject `exec`, or has GH_TOKEN set '
1430
+ + 'without `getRemoveToken`. A test run must inject both to exercise deregistration — the default '
1431
+ + "path mints a real removal token with GH_TOKEN and runs each org directory's own config.cmd.",
1432
+ );
1433
+ }
1434
+
1435
+ if (!process.env.GH_TOKEN) {
1436
+ logger.warn(`Skipping org-side deregistration of ${targets.length} runner(s) — GH_TOKEN is not set.`);
1437
+ logger.warn(` Their directories are kept so a later uninstall can retry. To finish now, remove them under each org's Settings → Actions → Runners: ${targets.map((t) => t.org).join(', ')}`);
1438
+ return { deregistered: [], failed: targets };
1439
+ }
1440
+
1441
+ const removeToken = getRemoveToken || (async (org) => {
1442
+ const { getOctokit } = require('../utils/github.js');
1443
+ const { data } = await getOctokit().rest.actions.createRemoveTokenForOrg({ org });
1444
+ return data.token;
1445
+ });
1446
+ // `cmd.exe /c` for the same reason register-org uses it: Node's CreateProcess
1447
+ // cannot launch a .cmd file directly.
1448
+ const run = exec || ((cmd, args, cwd) => require('child_process').spawnSync(cmd, args, { cwd, stdio: 'inherit' }));
1449
+
1450
+ const deregistered = [];
1451
+ const failed = [];
1452
+ for (const target of targets) {
1453
+ const { org, dir, configCmd } = target;
1454
+ try {
1455
+ const token = await removeToken(org);
1456
+ const result = run('cmd.exe', ['/c', configCmd, 'remove', '--token', token], dir) || {};
1457
+ if (result.error) throw result.error;
1458
+ if (result.status !== 0) {
1459
+ const code = result.status === null ? 'null (killed)' : result.status;
1460
+ logger.warn(` ✗ ${org}: config.cmd remove exited ${code} — still registered, keeping ${dir} to retry`);
1461
+ failed.push(target);
1462
+ continue;
1463
+ }
1464
+ logger.log(` ✓ Deregistered ${org} from GitHub`);
1465
+ deregistered.push(org);
1466
+ } catch (e) {
1467
+ logger.warn(` ✗ ${org}: ${e.message} — still registered, keeping ${dir} to retry`);
1468
+ failed.push(target);
1469
+ }
1470
+ }
1471
+ return { deregistered, failed };
1472
+ }
1473
+
1474
+ function writeRunnerStartupShortcut({ runnerName, runnerDir }) {
1475
+ if (process.platform !== 'win32') return;
1476
+ const runCmd = path.join(runnerDir, 'run.cmd');
1477
+ if (!jetpack.exists(runCmd)) {
1478
+ throw new Error(`Cannot write Startup shortcut — run.cmd not found at ${runCmd}.`);
1479
+ }
1480
+ jetpack.dir(STARTUP_DIR);
1481
+ const file = runnerStartupFile(runnerName);
1482
+ // /D <runnerDir> sets the new process's cwd to the runner dir itself.
1483
+ // actions/runner's auto-update writes its `update.finished` marker to
1484
+ // cwd — if cwd is anywhere not user-writable (we tried %WINDIR% earlier),
1485
+ // the write fails with Access Denied and the runner reconnect-loops
1486
+ // forever. The runner dir is the documented expected cwd. uninstall has
1487
+ // killRunnerProcessesUnderHome to clear the cwd lock when needed.
1488
+ const body = [
1489
+ '@echo off',
1490
+ `start "" /min /D "${runnerDir}" "${runCmd}"`,
1491
+ '',
1492
+ ].join('\r\n');
1493
+ jetpack.write(file, body);
1494
+ logger.log(` ✓ Startup shortcut: ${file}`);
1495
+ }
1496
+
1497
+ function removeRunnerStartupShortcut(runnerName) {
1498
+ if (process.platform !== 'win32') return false;
1499
+ const file = runnerStartupFile(runnerName);
1500
+ if (jetpack.exists(file)) {
1501
+ jetpack.remove(file);
1502
+ return true;
1503
+ }
1504
+ return false;
1505
+ }
1506
+
1507
+ // Names of the `<prefix>*.cmd` shortcuts in the Startup folder (sans `.cmd`).
1508
+ // `startupDir` is injected by the tests; off Windows there is no Startup folder
1509
+ // to ask, so a real call answers with nothing.
1510
+ function listStartupShortcuts(prefix, startupDir) {
1511
+ const dir = startupDir === undefined ? STARTUP_DIR : startupDir;
1512
+ if (!dir || !jetpack.exists(dir)) return [];
1513
+ const lower = prefix.toLowerCase();
1514
+ return (jetpack.list(dir) || [])
1515
+ .filter((name) => /\.cmd$/i.test(name))
1516
+ .filter((name) => name.toLowerCase().startsWith(lower) && name.length > `${prefix}.cmd`.length)
1517
+ .map((name) => name.replace(/\.cmd$/i, ''))
1518
+ .sort();
1519
+ }
1520
+
1521
+ // The omega runners' shortcuts: `omega-runner-<host>-<org>`.
1522
+ function listRunnerStartupShortcuts(startupDir) {
1523
+ return listStartupShortcuts('omega-runner-', startupDir);
1524
+ }
1525
+
1526
+ // The electron-manager era's: `em-runner-<host>-<org>`, which `status` names
1527
+ // and `uninstall` removes — never anything `start` would launch.
1528
+ function listLegacyRunnerStartupShortcuts(startupDir) {
1529
+ return listStartupShortcuts(LEGACY_RUNNER_PREFIX, startupDir);
1530
+ }
1531
+
1532
+ // Spawn Runner.Listener.exe as a fully detached background process. We exec
1533
+ // the listener directly (NOT via run.cmd) because run.cmd is a cmd.exe batch
1534
+ // wrapper that blocks for the lifetime of the listener with cwd = runnerDir;
1535
+ // a long-lived cmd.exe holding cwd inside RUNNER_HOME blocks every later
1536
+ // uninstall with EPERM. Bypassing run.cmd loses its self-update relaunch
1537
+ // path, but `mgr runner install` refreshing the runner binary is the
1538
+ // preferred update mechanism in @omega.js/desktop anyway.
1539
+ //
1540
+ // Detached + windowsHide + ignored stdio = no console window flashes during
1541
+ // install and the listener survives the install command's exit. UAC-
1542
+ // elevated parents still spawn into Session 1 because UAC only changes the
1543
+ // token, not the session — confirmed empirically.
1544
+ function spawnRunnerDetached(runnerDir, home) {
1545
+ if (process.platform !== 'win32') {
1546
+ return { ok: false, message: 'not-windows', pid: null };
1547
+ }
1548
+ const runCmd = path.join(runnerDir, 'run.cmd');
1549
+ if (!jetpack.exists(runCmd)) {
1550
+ return { ok: false, message: `run.cmd not found at ${runCmd}`, pid: null };
1551
+ }
1552
+ const { spawn } = require('child_process');
1553
+ try {
1554
+ // cwd is the runner dir itself. actions/runner's auto-update path writes
1555
+ // a marker file `update.finished` to cwd; if cwd is %WINDIR% the write
1556
+ // fails with Access Denied and the runner enters a 5-second reconnect
1557
+ // loop forever. The runner dir is the documented expected cwd. The
1558
+ // tradeoff — that the cmd.exe wrapper holds the dir handle for the
1559
+ // lifetime of the listener — is handled in uninstall via
1560
+ // killRunnerProcessesUnderHome which kills the wrapper too.
1561
+ // HOME is the runner's private one ([#807](https://github.com/Omega-JS-Stack/omega/issues/807)),
1562
+ // passed here as well as written to `<runner dir>\.env`: the listener's own
1563
+ // environment is what reaches its jobs, so the spawn never depends on the
1564
+ // `.env` read alone.
1565
+ const child = spawn('cmd.exe', ['/c', runCmd], {
1566
+ cwd: runnerDir,
1567
+ env: { ...process.env, HOME: runnerPrivateHome(home || RUNNER_HOME) },
1568
+ detached: true,
1569
+ stdio: 'ignore',
1570
+ windowsHide: true,
1571
+ });
1572
+ child.unref();
1573
+ return { ok: true, pid: child.pid };
1574
+ } catch (e) {
1575
+ return { ok: false, message: e.message, pid: null };
1576
+ }
1577
+ }
1578
+
1579
+ // Enumerate Runner.Listener.exe processes whose ExecutablePath lives under
1580
+ // `targetDir`. Used by the per-org state (visibility) and stop (kill targets).
1581
+ // Returns [{ pid, sessionId, execPath }, ...]. Processes whose path we cannot
1582
+ // read are surfaced too, with execPath = null, so callers can decide what to do
1583
+ // with them — status warns, stop kills.
1584
+ function listRunnerListenerProcessesUnder(targetDir) {
1585
+ if (process.platform !== 'win32') return [];
1586
+ const { spawnSync } = require('child_process');
1587
+ const r = spawnSync('powershell', [
1588
+ '-NoProfile', '-NonInteractive', '-Command',
1589
+ `Get-CimInstance Win32_Process -Filter "Name='Runner.Listener.exe'" | ForEach-Object { "$($_.ProcessId)|$($_.SessionId)|$($_.ExecutablePath)" }`,
1590
+ ], { encoding: 'utf8' });
1591
+ if (r.status !== 0) return [];
1592
+ const out = [];
1593
+ for (const rawLine of (r.stdout || '').split(/\r?\n/)) {
1594
+ const line = rawLine.trim();
1595
+ if (!line) continue;
1596
+ const parts = line.split('|');
1597
+ if (parts.length !== 3) continue;
1598
+ const [pid, sessionId, execPath] = parts;
1599
+ if (!/^\d+$/.test(pid)) continue;
1600
+ if (!execPath) {
1601
+ // Path unavailable — the process belongs to another account. Surface it
1602
+ // so status can call it out and stop can kill it. Never under a test run:
1603
+ // this is `listenerFilterScript`'s `includeUnreadable` arm in JS, one rule
1604
+ // in two places, and a test may not kill a stranger's runner.
1605
+ if (isTestRun()) continue;
1606
+ out.push({ pid: parseInt(pid, 10), sessionId: parseInt(sessionId, 10), execPath: null });
1607
+ continue;
1608
+ }
1609
+ if (isListenerUnder(execPath, targetDir)) {
1610
+ out.push({ pid: parseInt(pid, 10), sessionId: parseInt(sessionId, 10), execPath });
1611
+ }
1612
+ }
1613
+ return out;
1614
+ }
1615
+
1616
+ // Does this listener live INSIDE that runner dir? A bare prefix match said yes
1617
+ // to the wrong org: `actions-runner-acme` claimed every listener under
1618
+ // `actions-runner-acme-2`, so `start` read the long org's runner as the short
1619
+ // org's and left the short one offline
1620
+ // ([#801](https://github.com/Omega-JS-Stack/omega/issues/801)). The path must
1621
+ // continue into the directory, and the separators are normalised because the
1622
+ // box's paths are Windows ones whichever OS is asking.
1623
+ function isListenerUnder(execPath, targetDir) {
1624
+ const normalize = (p) => String(p).replace(/[\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
1625
+ return normalize(execPath).startsWith(`${normalize(targetDir)}/`);
1626
+ }
1627
+
1628
+ // Idempotent cleanup of any omega-runner-* Scheduled Tasks left over from
1629
+ // v1.2.16–v1.2.34, which registered runners as Logon Tasks. v1.2.35+ moved
1630
+ // to Startup folder shortcuts; this runs during uninstall so upgraders'
1631
+ // leftover tasks get pruned without manual schtasks juggling.
1632
+ async function uninstallLegacyLogonTasks() {
1633
+ if (process.platform !== 'win32') return;
1634
+ const { spawnSync } = require('child_process');
1635
+ const r = spawnSync('schtasks', ['/Query', '/FO', 'CSV', '/NH'], { encoding: 'utf8' });
1636
+ if (r.status !== 0) return;
1637
+ const tasks = [];
1638
+ for (const rawLine of (r.stdout || '').split(/\r?\n/)) {
1639
+ const line = rawLine.trim();
1640
+ if (!line) continue;
1641
+ const m = /^"([^"]+)"/.exec(line);
1642
+ if (!m) continue;
1643
+ const tn = m[1].replace(/^\\/, '');
1644
+ if (/^omega-runner-/i.test(tn)) tasks.push(tn);
1645
+ }
1646
+ if (tasks.length === 0) return;
1647
+ for (const name of tasks) {
1648
+ logger.log(`Removing legacy Logon Task ${name}…`);
1649
+ spawnSync('schtasks', ['/End', '/TN', name], { stdio: 'ignore' });
1650
+ const d = spawnSync('schtasks', ['/Delete', '/TN', name, '/F'], { encoding: 'utf8' });
1651
+ if (d.status !== 0) {
1652
+ logger.warn(` schtasks /Delete failed for ${name} (exit ${d.status}): ${(d.stderr || '').trim().slice(0, 200)}`);
1653
+ } else {
1654
+ logger.log(` ✓ Deleted ${name}`);
1655
+ }
1656
+ }
1657
+ }
1658
+
1659
+ function readConfig(home) {
1660
+ const file = path.join(home || RUNNER_HOME, 'config.json');
1661
+ if (!jetpack.exists(file)) return {};
1662
+ return jetpack.read(file, 'json') || {};
1663
+ }
1664
+
1665
+ function saveConfig(data, home) {
1666
+ const file = path.join(home || RUNNER_HOME, 'config.json');
1667
+ const cur = readConfig(home);
1668
+ jetpack.write(file, { ...cur, ...data });
1669
+ }
1670
+
1671
+ // ─── monitor ────────────────────────────────────────────────────────────────────
1672
+ //
1673
+ // `npx omega runner monitor` — pretty-prints the JSONL signing event log in real time.
1674
+ //
1675
+ // Reads the same path `sign-windows` writes to (see sign-events.js for the full
1676
+ // resolution chain). Uses signEvents.getLogPath() so it always stays in sync.
1677
+ //
1678
+ // Designed to run from a regular PowerShell / cmd / Windows Terminal session on the
1679
+ // signing box. Reads the file, prints existing events first (so you see context if
1680
+ // signing already started), then watches for new lines via fs.watchFile + offset
1681
+ // tracking. No fancy deps.
1682
+ async function monitor(options) {
1683
+ options = options || {};
1684
+ const home = options._home || RUNNER_HOME;
1685
+ const signEvents = require('../lib/sign-helpers/sign-events.js');
1686
+ // `_home` redirects the roster below, never this path: `getLogPath()` takes no
1687
+ // argument and froze its answer at require time from the environment —
1688
+ // `OMEGA_SIGN_LOG`, else `<OMEGA_RUNNER_HOME>\omega-signing.log`, else the
1689
+ // platform default (lib/sign-helpers/sign-events.js). Pass `file` to move it.
1690
+ const file = options.file || signEvents.getLogPath();
1691
+ const followOnly = !!options['follow-only'];
1692
+
1693
+ logger.log(`Watching: ${file}`);
1694
+
1695
+ // List the registered orgs and the live state of each, so the user can see
1696
+ // exactly which orgs the monitor will pick up signing events from.
1697
+ const report = orgRunnerReport({ home });
1698
+ if (report.length === 0) {
1699
+ logger.log('(no orgs registered yet — run `npx omega runner install` first)');
1700
+ } else {
1701
+ logger.log(`Monitoring signing requests across ${report.length} org(s):`);
1702
+ for (const { state, lines } of report) {
1703
+ for (const line of lines) (state.sessionZero ? logger.warn : logger.log).call(logger, line);
1704
+ }
1705
+ if (report.some((r) => r.state.sessionZero)) {
1706
+ logger.warn(` ⚠ Kill it ('npx omega runner stop') and re-spawn from Session 1, or it will fail every job it picks up.`);
1707
+ }
1708
+ }
1709
+
1710
+ if (!fs.existsSync(file)) {
1711
+ // Make sure the parent dir exists so events written before monitor sees the file
1712
+ // don't fail (sign-events.js handles its own write errors, but pre-creating the dir
1713
+ // avoids a confusing "waiting forever" UX when OMEGA_RUNNER_HOME hasn't been used yet).
1714
+ try {
1715
+ jetpack.dir(path.dirname(file));
1716
+ } catch (_) { /* best-effort */ }
1717
+ logger.log('(file does not exist yet — waiting for first sign event...)');
1718
+ }
1719
+
1720
+ // Track byte offset so we only print new lines on each poll. Initialize to either
1721
+ // 0 (replay everything) or the file's current size (--follow-only — only show new
1722
+ // events).
1723
+ let pos = followOnly && fs.existsSync(file)
1724
+ ? fs.statSync(file).size
1725
+ : 0;
1726
+
1727
+ let inFlight = false;
1728
+ let buffer = '';
1729
+
1730
+ function pump() {
1731
+ if (inFlight) return;
1732
+ if (!fs.existsSync(file)) return;
1733
+ inFlight = true;
1734
+ const stream = fs.createReadStream(file, { start: pos });
1735
+ stream.on('data', (chunk) => {
1736
+ buffer += chunk.toString('utf8');
1737
+ let nl;
1738
+ while ((nl = buffer.indexOf('\n')) !== -1) {
1739
+ const line = buffer.slice(0, nl);
1740
+ buffer = buffer.slice(nl + 1);
1741
+ if (line.trim()) renderLine(line);
1742
+ pos += Buffer.byteLength(line, 'utf8') + 1;
1743
+ }
1744
+ });
1745
+ stream.on('end', () => { inFlight = false; });
1746
+ stream.on('error', (e) => {
1747
+ inFlight = false;
1748
+ logger.warn(`monitor read failed: ${e.message}`);
1749
+ });
1750
+ }
1751
+
1752
+ pump();
1753
+
1754
+ // Watch for size changes. fs.watchFile polls (default 5s) which is fine here —
1755
+ // we don't need sub-second latency on a sign-monitor.
1756
+ fs.watchFile(file, { interval: 500 }, (curr, prev) => {
1757
+ // File rotated / truncated — reset.
1758
+ if (curr.size < pos) {
1759
+ pos = 0;
1760
+ buffer = '';
1761
+ logger.log('(log truncated — replaying from start)');
1762
+ }
1763
+ if (curr.size > pos) pump();
1764
+ });
1765
+
1766
+ // Keep alive forever.
1767
+ await new Promise(() => {});
1768
+ }
1769
+
1770
+ function renderLine(jsonLine) {
1771
+ let evt;
1772
+ try {
1773
+ evt = JSON.parse(jsonLine);
1774
+ } catch (_) {
1775
+ process.stdout.write(`[??] ${jsonLine}\n`);
1776
+ return;
1777
+ }
1778
+
1779
+ const ts = (evt.ts || '').replace('T', ' ').replace('Z', '');
1780
+ const dur = typeof evt.duration_ms === 'number' ? ` (${formatDuration(evt.duration_ms)})` : '';
1781
+ const fmt = logger.format || {};
1782
+ const c = (col, str) => (fmt[col] ? fmt[col](str) : str);
1783
+
1784
+ switch (evt.event) {
1785
+ case 'job-start':
1786
+ process.stdout.write(`\n${c('cyan', '━'.repeat(60))}\n`);
1787
+ process.stdout.write(`${c('cyan', `[${ts}] JOB START`)}`);
1788
+ // Org/repo callout: prefer GH-provided env, fall back to parsing the workspace path
1789
+ // (<runner home>\actions-runner-<org>\_work\<repo>\<repo>) when running outside
1790
+ // a GH Actions context (e.g. local smoke tests).
1791
+ let org = evt.github_owner || null;
1792
+ let repo = evt.github_repo || null;
1793
+ if (!org && evt.runner_workspace) {
1794
+ const m = /actions-runner-([^\\/]+)[\\/]_work[\\/]([^\\/]+)/i.exec(evt.runner_workspace);
1795
+ if (m) { org = m[1]; repo = repo || m[2]; }
1796
+ }
1797
+ if (org || repo) {
1798
+ const label = [org, repo].filter(Boolean).join('/');
1799
+ process.stdout.write(` ${c('yellow', label)}`);
1800
+ }
1801
+ if (evt.github_workflow) process.stdout.write(c('gray', ` workflow=${evt.github_workflow}`));
1802
+ if (evt.github_run_id) process.stdout.write(c('gray', ` run=${evt.github_run_id}`));
1803
+ if (evt.runner_workspace) process.stdout.write(c('gray', ` workspace=${evt.runner_workspace}`));
1804
+ process.stdout.write('\n');
1805
+ break;
1806
+ case 'job-end':
1807
+ const ok = evt.ok ? c('green', 'OK') : c('red', 'FAILED');
1808
+ process.stdout.write(`${c('cyan', `[${ts}] JOB END ${ok}${dur}`)}\n`);
1809
+ if (evt.error) process.stdout.write(` ${c('red', evt.error)}\n`);
1810
+ process.stdout.write(`${c('cyan', '━'.repeat(60))}\n`);
1811
+ break;
1812
+ case 'sign-start':
1813
+ process.stdout.write(`[${ts}] ${c('yellow', '→')} sign ${c('white', evt.file)}`);
1814
+ if (typeof evt.bytes === 'number') process.stdout.write(c('gray', ` (${formatBytes(evt.bytes)})`));
1815
+ process.stdout.write(c('gray', ` mode=${evt.mode}`));
1816
+ process.stdout.write('\n');
1817
+ break;
1818
+ case 'sign-attempt':
1819
+ // Only worth a line when it is not the first try — attempt 1 is the
1820
+ // sign-start line you already saw.
1821
+ if (evt.attempt > 1) {
1822
+ process.stdout.write(`[${ts}] ${c('yellow', '↻')} attempt ${evt.attempt}/${evt.of} ${c('white', evt.file)}\n`);
1823
+ }
1824
+ break;
1825
+ case 'sign-retry':
1826
+ process.stdout.write(`[${ts}] ${c('yellow', '↻')} retrying ${c('white', evt.file)} ${c('gray', `(attempt ${evt.attempt}/${evt.of} failed, waiting ${formatDuration(evt.retry_in_ms)})`)}\n`);
1827
+ if (evt.error) process.stdout.write(` ${c('gray', evt.error)}\n`);
1828
+ break;
1829
+ case 'sign-done':
1830
+ process.stdout.write(`[${ts}] ${c('green', '✓')} signed ${c('white', evt.file)}${c('gray', dur)}\n`);
1831
+ break;
1832
+ case 'sign-fail':
1833
+ process.stdout.write(`[${ts}] ${c('red', '✗')} FAILED ${c('white', evt.file)} ${c('gray', `(phase:${evt.phase}${dur})`)}\n`);
1834
+ if (evt.error) process.stdout.write(` ${c('red', evt.error)}\n`);
1835
+ break;
1836
+ default:
1837
+ process.stdout.write(`[${ts}] ${evt.event} ${JSON.stringify(evt)}\n`);
1838
+ }
1839
+ }
1840
+
1841
+ function formatBytes(n) {
1842
+ if (n < 1024) return `${n}B`;
1843
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
1844
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
1845
+ }
1846
+
1847
+ function formatDuration(ms) {
1848
+ if (ms < 1000) return `${ms}ms`;
1849
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
1850
+ return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
1851
+ }
1852
+
1853
+ // Exports for testing.
1854
+ module.exports.RUNNER_LABELS = RUNNER_LABELS;
1855
+ module.exports.RUNNER_HOME = RUNNER_HOME; // the home this process froze at require time — the suites assert it is a scratch
1856
+ module.exports.STARTUP_DIR = STARTUP_DIR; // the Startup folder it froze at the same moment — no home scopes it, so the suites assert it too
1857
+ module.exports.isScratchRunnerHome = isScratchRunnerHome;
1858
+ module.exports.ACTIONS_RUNNER_VERSION = ACTIONS_RUNNER_VERSION;
1859
+ module.exports.defaultRunnerHome = defaultRunnerHome;
1860
+ module.exports.orgRunnerState = orgRunnerState;
1861
+ module.exports.monitorOrgStates = monitorOrgStates;
1862
+ module.exports.orgRunnerStateLines = orgRunnerStateLines;
1863
+ module.exports.orgRunnerReport = orgRunnerReport;
1864
+ module.exports.listOrgRunnerDirs = listOrgRunnerDirs;
1865
+ module.exports.deregisterOrgRunners = deregisterOrgRunners;
1866
+ module.exports.removeRunnerHomeWithRetry = removeRunnerHomeWithRetry;
1867
+ module.exports.LEGACY_RUNNER_PREFIX = LEGACY_RUNNER_PREFIX;
1868
+ module.exports.listLegacyRunnerHomes = listLegacyRunnerHomes;
1869
+ module.exports.selectRunnerOrgs = selectRunnerOrgs;
1870
+ module.exports.startPreflight = startPreflight;
1871
+ module.exports.listRunnerStartupShortcuts = listRunnerStartupShortcuts;
1872
+ module.exports.isListenerUnder = isListenerUnder; // the dir-containment rule start reads a live listener with
1873
+ module.exports.listLegacyRunnerStartupShortcuts = listLegacyRunnerStartupShortcuts;
1874
+ module.exports.listenerFilterScript = listenerFilterScript;