six-degrees 0.2.0 → 0.3.0-beta.1

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 (478) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +10 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +39 -11
  5. package/.next/standalone/.next/required-server-files.json +4 -1
  6. package/.next/standalone/.next/routes-manifest.json +60 -0
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-delete/route.js +3 -2
  22. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/admin-update/route.js +3 -2
  24. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/bridges/route.js +3 -2
  26. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/bulk-import/route.js +3 -2
  28. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/company-scores/legacy/route/app-paths-manifest.json +3 -0
  30. package/.next/standalone/.next/server/app/api/company-scores/legacy/route/build-manifest.json +12 -0
  31. package/.next/standalone/.next/server/app/api/company-scores/legacy/route/server-reference-manifest.json +4 -0
  32. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +9 -0
  33. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.map +5 -0
  34. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -0
  35. package/.next/standalone/.next/server/app/api/company-scores/legacy/route_client-reference-manifest.js +3 -0
  36. package/.next/standalone/.next/server/app/api/company-scores/route.js +3 -2
  37. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/connections/route.js +3 -2
  39. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/data/export/route/app-paths-manifest.json +3 -0
  41. package/.next/standalone/.next/server/app/api/data/export/route/build-manifest.json +12 -0
  42. package/.next/standalone/.next/server/app/api/data/export/route/server-reference-manifest.json +4 -0
  43. package/.next/standalone/.next/server/app/api/data/export/route.js +7 -0
  44. package/.next/standalone/.next/server/app/api/data/export/route.js.map +5 -0
  45. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -0
  46. package/.next/standalone/.next/server/app/api/data/export/route_client-reference-manifest.js +3 -0
  47. package/.next/standalone/.next/server/app/api/data/import/route/app-paths-manifest.json +3 -0
  48. package/.next/standalone/.next/server/app/api/data/import/route/build-manifest.json +12 -0
  49. package/.next/standalone/.next/server/app/api/data/import/route/server-reference-manifest.json +4 -0
  50. package/.next/standalone/.next/server/app/api/data/import/route.js +8 -0
  51. package/.next/standalone/.next/server/app/api/data/import/route.js.map +5 -0
  52. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -0
  53. package/.next/standalone/.next/server/app/api/data/import/route_client-reference-manifest.js +3 -0
  54. package/.next/standalone/.next/server/app/api/data/restart/route/app-paths-manifest.json +3 -0
  55. package/.next/standalone/.next/server/app/api/data/restart/route/build-manifest.json +12 -0
  56. package/.next/standalone/.next/server/app/api/data/restart/route/server-reference-manifest.json +4 -0
  57. package/.next/standalone/.next/server/app/api/data/restart/route.js +8 -0
  58. package/.next/standalone/.next/server/app/api/data/restart/route.js.map +5 -0
  59. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -0
  60. package/.next/standalone/.next/server/app/api/data/restart/route_client-reference-manifest.js +3 -0
  61. package/.next/standalone/.next/server/app/api/data/reveal/route/app-paths-manifest.json +3 -0
  62. package/.next/standalone/.next/server/app/api/data/reveal/route/build-manifest.json +12 -0
  63. package/.next/standalone/.next/server/app/api/data/reveal/route/server-reference-manifest.json +4 -0
  64. package/.next/standalone/.next/server/app/api/data/reveal/route.js +7 -0
  65. package/.next/standalone/.next/server/app/api/data/reveal/route.js.map +5 -0
  66. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -0
  67. package/.next/standalone/.next/server/app/api/data/reveal/route_client-reference-manifest.js +3 -0
  68. package/.next/standalone/.next/server/app/api/data/route/app-paths-manifest.json +3 -0
  69. package/.next/standalone/.next/server/app/api/data/route/build-manifest.json +12 -0
  70. package/.next/standalone/.next/server/app/api/data/route/server-reference-manifest.json +4 -0
  71. package/.next/standalone/.next/server/app/api/data/route.js +7 -0
  72. package/.next/standalone/.next/server/app/api/data/route.js.map +5 -0
  73. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -0
  74. package/.next/standalone/.next/server/app/api/data/route_client-reference-manifest.js +3 -0
  75. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +3 -2
  76. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/api/ingest/route.js +2 -2
  78. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/api/network/route.js +3 -2
  80. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  81. package/.next/standalone/.next/server/app/api/notifications/route.js +3 -2
  82. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -2
  84. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/api/queue/route.js +3 -2
  86. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
  88. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  89. package/.next/standalone/.next/server/app/api/settings/route/app-paths-manifest.json +3 -0
  90. package/.next/standalone/.next/server/app/api/settings/route/build-manifest.json +12 -0
  91. package/.next/standalone/.next/server/app/api/settings/route/server-reference-manifest.json +4 -0
  92. package/.next/standalone/.next/server/app/api/settings/route.js +9 -0
  93. package/.next/standalone/.next/server/app/api/settings/route.js.map +5 -0
  94. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -0
  95. package/.next/standalone/.next/server/app/api/settings/route_client-reference-manifest.js +3 -0
  96. package/.next/standalone/.next/server/app/api/settings/sector-preview/route/app-paths-manifest.json +3 -0
  97. package/.next/standalone/.next/server/app/api/settings/sector-preview/route/build-manifest.json +12 -0
  98. package/.next/standalone/.next/server/app/api/settings/sector-preview/route/server-reference-manifest.json +4 -0
  99. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +8 -0
  100. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.map +5 -0
  101. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -0
  102. package/.next/standalone/.next/server/app/api/settings/sector-preview/route_client-reference-manifest.js +3 -0
  103. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/app-paths-manifest.json +3 -0
  104. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/build-manifest.json +12 -0
  105. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/server-reference-manifest.json +4 -0
  106. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +8 -0
  107. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.map +5 -0
  108. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -0
  109. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route_client-reference-manifest.js +3 -0
  110. package/.next/standalone/.next/server/app/api/setup-profile/route.js +3 -2
  111. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/api/unlock/route.js +3 -2
  113. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/api/update/route.js +1 -1
  115. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  116. package/.next/standalone/.next/server/app/api/update-images/route.js +3 -2
  117. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  118. package/.next/standalone/.next/server/app/api/users/route.js +3 -2
  119. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  120. package/.next/standalone/.next/server/app/avatars/[file]/route.js +2 -1
  121. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  122. package/.next/standalone/.next/server/app/favicon.ico/route.js +2 -1
  123. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  124. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  125. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  126. package/.next/standalone/.next/server/app/import.html +1 -1
  127. package/.next/standalone/.next/server/app/import.rsc +13 -13
  128. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  129. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  130. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
  131. package/.next/standalone/.next/server/app/index.html +1 -1
  132. package/.next/standalone/.next/server/app/index.rsc +13 -13
  133. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
  134. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  135. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  136. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  137. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  138. package/.next/standalone/.next/server/app/launch.html +2 -2
  139. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  140. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  141. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  142. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
  143. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  144. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  145. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  146. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  147. package/.next/standalone/.next/server/app/paths.html +1 -1
  148. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  149. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  150. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  151. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
  152. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  153. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  154. package/.next/standalone/.next/server/app/profile.html +1 -1
  155. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  156. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  157. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  158. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
  159. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  160. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  161. package/.next/standalone/.next/server/app/queue.html +1 -1
  162. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  163. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  164. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  165. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
  166. package/.next/standalone/.next/server/app/settings/page/app-paths-manifest.json +3 -0
  167. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +20 -0
  168. package/.next/standalone/.next/server/app/settings/page/next-font-manifest.json +11 -0
  169. package/.next/standalone/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
  170. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -0
  171. package/.next/standalone/.next/server/app/settings/page.js +13 -0
  172. package/.next/standalone/.next/server/app/settings/page.js.map +5 -0
  173. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -0
  174. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +3 -0
  175. package/.next/standalone/.next/server/app/settings.html +1 -0
  176. package/.next/standalone/.next/server/app/settings.meta +26 -0
  177. package/.next/standalone/.next/server/app/settings.rsc +24 -0
  178. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +24 -0
  179. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +4 -0
  180. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +28 -0
  181. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  182. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  183. package/.next/standalone/.next/server/app/setup.html +1 -1
  184. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  185. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  186. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  187. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
  188. package/.next/standalone/.next/server/app-paths-manifest.json +10 -0
  189. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +3 -0
  190. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0i5hdu4._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +3 -0
  198. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +3 -0
  199. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +3 -0
  200. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +8 -0
  201. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +3 -0
  202. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +3 -0
  203. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +3 -0
  204. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +3 -0
  205. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +3 -0
  206. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +3 -0
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +3 -0
  208. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +3 -0
  209. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +8 -0
  210. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +3 -0
  211. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +3 -0
  212. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +3 -0
  213. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +3 -0
  214. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +3 -0
  215. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +3 -0
  216. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +3 -0
  217. package/.next/standalone/.next/server/chunks/_072jl0y._.js +3 -0
  218. package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +3 -0
  219. package/.next/standalone/.next/server/chunks/_1_fynia._.js +21 -3
  220. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +3 -0
  221. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +2 -1
  222. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  223. package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +3 -0
  224. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_company-scores_legacy_route_actions_1ncspu_.js +3 -0
  225. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_export_route_actions_1lkimty.js +3 -0
  226. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_import_route_actions_1eji0kd.js +3 -0
  227. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_restart_route_actions_00_-u6o.js +3 -0
  228. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_reveal_route_actions_0-moqtn.js +3 -0
  229. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_route_actions_0j0w5ep.js +3 -0
  230. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_route_actions_1bpm9ac.js +3 -0
  231. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_sector-preview_route_actions_0w79454.js +3 -0
  232. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_sector-suggestions_route_actions_1eoqugp.js +3 -0
  233. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +155 -0
  234. package/.next/standalone/.next/server/chunks/{lib_rpc_0mc73lk.js → lib_rpc_00tx2sm.js} +1 -1
  235. package/.next/standalone/.next/server/chunks/lib_rpc_02-qa4d.js +3 -0
  236. package/.next/standalone/.next/server/chunks/lib_rpc_02ndsg2.js +3 -0
  237. package/.next/standalone/.next/server/chunks/lib_rpc_08q5vp6.js +3 -0
  238. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +3 -0
  239. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -2
  240. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +3 -0
  241. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +3 -0
  242. package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +3 -0
  243. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +3 -0
  244. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +3 -0
  245. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +3 -0
  246. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +3 -0
  247. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +3 -0
  248. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +3 -0
  249. package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +158 -0
  250. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0mocm6d._.js → [root-of-the-server]__0an6blh._.js} +3 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +1 -1
  252. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +3 -0
  253. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bipr5s._.js → [root-of-the-server]__1z1j3ek._.js} +2 -2
  254. package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +3 -0
  255. package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +3 -0
  256. package/.next/standalone/.next/server/chunks/ssr/{_0ggabjc._.js → _0942v8n._.js} +1 -1
  257. package/.next/standalone/.next/server/chunks/ssr/{_1djpk8t._.js → _0hl_t7r._.js} +2 -2
  258. package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +132 -0
  259. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +3 -0
  260. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
  261. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_11x4dq5.js +3 -0
  262. package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
  263. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +2 -2
  264. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +4 -4
  265. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  266. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  267. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +3 -0
  268. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  269. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  270. package/.next/standalone/.next/server/chunks/ssr/node_modules_1f8t_rp._.js +23 -0
  271. package/.next/standalone/.next/server/chunks/ssr/node_modules_1nkgb45._.js +2 -2
  272. package/.next/standalone/.next/server/chunks/ssr/{node_modules_01xbj_9._.js → node_modules_1wax83z._.js} +3 -3
  273. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_00nap8h._.js → node_modules_next_dist_1bunoni._.js} +2 -2
  274. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0pco7a6._.js +2 -2
  275. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  276. package/.next/standalone/.next/server/middleware-manifest.json +15 -7
  277. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  278. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  279. package/.next/standalone/.next/server/pages/404.html +1 -1
  280. package/.next/standalone/.next/server/pages/500.html +1 -1
  281. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  282. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  283. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  284. package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +1 -0
  285. package/.next/{static/chunks/34at5d2ifl62i.js → standalone/.next/static/chunks/0ngx9s6-zgshe.js} +5 -5
  286. package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +2 -0
  287. package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +1 -0
  288. package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +44 -0
  289. package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +1 -0
  290. package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +1 -0
  291. package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +1 -0
  292. package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +1 -0
  293. package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +16 -0
  294. package/.next/standalone/.next/static/chunks/2jz311xl1pp_8.js +1 -0
  295. package/.next/standalone/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
  296. package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +2 -0
  297. package/.next/standalone/.next/static/frfHFnRYTA0Wjrw8oZJtY/_clientMiddlewareManifest.js +14 -0
  298. package/.next/standalone/AGENTS.md +10 -0
  299. package/.next/standalone/CHANGELOG.md +325 -0
  300. package/.next/standalone/CONTRIBUTING.md +14 -2
  301. package/.next/standalone/README.md +181 -53
  302. package/.next/standalone/SECURITY.md +120 -10
  303. package/.next/standalone/app/api/company-scores/legacy/route.js +32 -0
  304. package/.next/standalone/app/api/company-scores/route.js +20 -11
  305. package/.next/standalone/app/api/data/export/route.js +49 -0
  306. package/.next/standalone/app/api/data/import/route.js +83 -0
  307. package/.next/standalone/app/api/data/restart/route.js +37 -0
  308. package/.next/standalone/app/api/data/reveal/route.js +14 -0
  309. package/.next/standalone/app/api/data/route.js +39 -0
  310. package/.next/standalone/app/api/ingest/route.js +15 -47
  311. package/.next/standalone/app/api/scraper/route.js +268 -128
  312. package/.next/standalone/app/api/settings/route.js +58 -0
  313. package/.next/standalone/app/api/settings/sector-preview/route.js +38 -0
  314. package/.next/standalone/app/api/settings/sector-suggestions/route.js +20 -0
  315. package/.next/standalone/app/api/update/route.js +197 -24
  316. package/.next/standalone/app/avatars/[file]/route.js +4 -2
  317. package/.next/standalone/app/components/CompanyScores.js +28 -7
  318. package/.next/standalone/app/components/EmptyState.js +8 -0
  319. package/.next/standalone/app/components/ForceGraph.js +40 -36
  320. package/.next/standalone/app/components/LegacyScoresCard.js +129 -0
  321. package/.next/standalone/app/components/Sidebar.js +19 -18
  322. package/.next/standalone/app/components/UpdatePanel.js +317 -99
  323. package/.next/standalone/app/components/UserProvider.js +4 -3
  324. package/.next/standalone/app/components/settings/DataSection.js +437 -0
  325. package/.next/standalone/app/components/settings/SectorSection.js +445 -0
  326. package/.next/standalone/app/components/ui.js +62 -0
  327. package/.next/standalone/app/import/page.js +3 -3
  328. package/.next/standalone/app/launch/page.js +30 -20
  329. package/.next/standalone/app/layout.js +2 -2
  330. package/.next/standalone/app/page.js +15 -2
  331. package/.next/standalone/app/paths/page.js +13 -5
  332. package/.next/standalone/app/profile/page.js +41 -186
  333. package/.next/standalone/app/queue/page.js +8 -2
  334. package/.next/standalone/app/settings/page.js +96 -0
  335. package/.next/standalone/app/setup/page.js +35 -19
  336. package/.next/standalone/bin/args.mjs +44 -0
  337. package/.next/standalone/bin/six-degrees.mjs +16 -17
  338. package/.next/standalone/install.sh +1 -1
  339. package/.next/standalone/lib/companies.js +44 -119
  340. package/.next/standalone/lib/csv.js +61 -27
  341. package/.next/standalone/lib/data-export.js +170 -0
  342. package/.next/standalone/lib/data-folder.js +238 -0
  343. package/.next/standalone/lib/data-import.js +878 -0
  344. package/.next/standalone/lib/db-client.js +48 -3
  345. package/.next/standalone/lib/durable.js +73 -0
  346. package/.next/standalone/lib/gate.js +90 -1
  347. package/.next/standalone/lib/ingest.js +40 -0
  348. package/.next/standalone/lib/legacy-offer.js +185 -0
  349. package/.next/standalone/lib/legacy-scores.js +77 -0
  350. package/.next/standalone/lib/linkedin-limits.js +124 -1
  351. package/.next/standalone/lib/release.js +5 -2
  352. package/.next/standalone/lib/rpc.js +121 -13
  353. package/.next/standalone/lib/sample-stats.js +16 -0
  354. package/.next/standalone/lib/scan-progress.js +13 -0
  355. package/.next/standalone/lib/scan-state.js +53 -0
  356. package/.next/standalone/lib/scanner-python.js +685 -0
  357. package/.next/standalone/lib/scanner-setup.js +104 -0
  358. package/.next/standalone/lib/scoring.js +747 -138
  359. package/.next/standalone/lib/scraper-client.js +10 -6
  360. package/.next/standalone/lib/sector-directory.js +1532 -0
  361. package/.next/standalone/lib/sector-focus.js +163 -0
  362. package/.next/standalone/lib/sector-labels.js +81 -0
  363. package/.next/standalone/lib/settings-effects.js +59 -0
  364. package/.next/standalone/lib/settings.js +83 -0
  365. package/.next/standalone/lib/updater-job.js +838 -0
  366. package/.next/standalone/lib/updater.js +494 -0
  367. package/.next/standalone/middleware.js +25 -28
  368. package/.next/standalone/next.config.mjs +19 -3
  369. package/.next/standalone/package-lock.json +103 -9
  370. package/.next/standalone/package.json +12 -11
  371. package/.next/standalone/public/demo-data.json +1 -1
  372. package/.next/standalone/scripts/apply-update.sh +482 -0
  373. package/.next/standalone/scripts/build-app.mjs +251 -9
  374. package/.next/standalone/scripts/gen-synthetic.mjs +16 -1
  375. package/.next/standalone/scripts/readme_buttons.py +89 -0
  376. package/.next/standalone/scripts/requirements.txt +106 -4
  377. package/.next/standalone/scripts/screenshots.py +2 -0
  378. package/.next/standalone/scripts/test-release-server.mjs +143 -0
  379. package/.next/standalone/server.js +1 -1
  380. package/.next/static/chunks/06x3jiiz96m5t.js +1 -0
  381. package/.next/{standalone/.next/static/chunks/34at5d2ifl62i.js → static/chunks/0ngx9s6-zgshe.js} +5 -5
  382. package/.next/static/chunks/0q-_khndfxubv.js +2 -0
  383. package/.next/static/chunks/1c79veqedvubt.js +1 -0
  384. package/.next/static/chunks/1chp3nx45meg5.js +44 -0
  385. package/.next/static/chunks/1irqj0lf-fu3d.js +1 -0
  386. package/.next/static/chunks/1uadqylrod5_v.js +1 -0
  387. package/.next/static/chunks/23u0zclgwpc4d.js +1 -0
  388. package/.next/static/chunks/289ktj97di6zg.js +1 -0
  389. package/.next/static/chunks/2b3lmf8e6uj8m.js +16 -0
  390. package/.next/static/chunks/2jz311xl1pp_8.js +1 -0
  391. package/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
  392. package/.next/static/chunks/3npq5yd7c4dmn.js +2 -0
  393. package/.next/static/frfHFnRYTA0Wjrw8oZJtY/_clientMiddlewareManifest.js +14 -0
  394. package/CHANGELOG.md +325 -0
  395. package/README.md +181 -53
  396. package/SECURITY.md +120 -10
  397. package/bin/args.mjs +44 -0
  398. package/bin/six-degrees.mjs +16 -17
  399. package/lib/companies.js +44 -119
  400. package/lib/csv.js +61 -27
  401. package/lib/data-export.js +170 -0
  402. package/lib/data-folder.js +238 -0
  403. package/lib/data-import.js +878 -0
  404. package/lib/db-client.js +48 -3
  405. package/lib/durable.js +73 -0
  406. package/lib/gate.js +90 -1
  407. package/lib/ingest.js +40 -0
  408. package/lib/legacy-offer.js +185 -0
  409. package/lib/legacy-scores.js +77 -0
  410. package/lib/linkedin-limits.js +124 -1
  411. package/lib/release.js +5 -2
  412. package/lib/rpc.js +121 -13
  413. package/lib/sample-stats.js +16 -0
  414. package/lib/scan-progress.js +13 -0
  415. package/lib/scan-state.js +53 -0
  416. package/lib/scanner-python.js +685 -0
  417. package/lib/scanner-setup.js +104 -0
  418. package/lib/scoring.js +747 -138
  419. package/lib/scraper-client.js +10 -6
  420. package/lib/sector-directory.js +1532 -0
  421. package/lib/sector-focus.js +163 -0
  422. package/lib/sector-labels.js +81 -0
  423. package/lib/settings-effects.js +59 -0
  424. package/lib/settings.js +83 -0
  425. package/lib/updater-job.js +838 -0
  426. package/lib/updater.js +494 -0
  427. package/package.json +12 -11
  428. package/public/demo-data.json +1 -1
  429. package/scripts/requirements.txt +106 -4
  430. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +0 -150
  431. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +0 -151
  432. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +0 -150
  433. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +0 -155
  434. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1zor0ys._.js +0 -3
  435. package/.next/standalone/.next/server/chunks/_0_rp-01._.js +0 -3
  436. package/.next/standalone/.next/server/chunks/_0hzdk79._.js +0 -3
  437. package/.next/standalone/.next/server/chunks/_0ow7u5r._.js +0 -3
  438. package/.next/standalone/.next/server/chunks/_0t0zc2d._.js +0 -3
  439. package/.next/standalone/.next/server/chunks/_0wa631d._.js +0 -3
  440. package/.next/standalone/.next/server/chunks/_13ccnao._.js +0 -3
  441. package/.next/standalone/.next/server/chunks/_13es063._.js +0 -3
  442. package/.next/standalone/.next/server/chunks/_142872j._.js +0 -3
  443. package/.next/standalone/.next/server/chunks/_18wsb_a._.js +0 -8
  444. package/.next/standalone/.next/server/chunks/_1e-4h5h._.js +0 -3
  445. package/.next/standalone/.next/server/chunks/_1gvvd7n._.js +0 -158
  446. package/.next/standalone/.next/server/chunks/_1moda66._.js +0 -3
  447. package/.next/standalone/.next/server/chunks/_1p6ghzv._.js +0 -3
  448. package/.next/standalone/.next/server/chunks/_1xmmi_v._.js +0 -3
  449. package/.next/standalone/.next/server/chunks/_1z-yrlf._.js +0 -3
  450. package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +0 -3
  451. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +0 -4
  452. package/.next/standalone/.next/static/N9wokeIiC9rUIGmzBhj6T/_clientMiddlewareManifest.js +0 -6
  453. package/.next/standalone/.next/static/chunks/1odow92u6wxz0.js +0 -1
  454. package/.next/standalone/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
  455. package/.next/standalone/.next/static/chunks/1zmpcmngg5c7-.js +0 -2
  456. package/.next/standalone/.next/static/chunks/220-ne82aus0c.js +0 -44
  457. package/.next/standalone/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
  458. package/.next/standalone/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
  459. package/.next/standalone/.next/static/chunks/3hsi90cejq2dg.js +0 -1
  460. package/.next/standalone/.next/static/chunks/3pfyuezyqd9ob.js +0 -1
  461. package/.next/standalone/.next/static/chunks/3ts-_4i6bfkh2.js +0 -1
  462. package/.next/standalone/scripts/score-connections.sql +0 -80
  463. package/.next/standalone/scripts/score_new_connections.sql +0 -77
  464. package/.next/static/N9wokeIiC9rUIGmzBhj6T/_clientMiddlewareManifest.js +0 -6
  465. package/.next/static/chunks/1odow92u6wxz0.js +0 -1
  466. package/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
  467. package/.next/static/chunks/1zmpcmngg5c7-.js +0 -2
  468. package/.next/static/chunks/220-ne82aus0c.js +0 -44
  469. package/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
  470. package/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
  471. package/.next/static/chunks/3hsi90cejq2dg.js +0 -1
  472. package/.next/static/chunks/3pfyuezyqd9ob.js +0 -1
  473. package/.next/static/chunks/3ts-_4i6bfkh2.js +0 -1
  474. package/scripts/score_new_connections.sql +0 -77
  475. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → frfHFnRYTA0Wjrw8oZJtY}/_buildManifest.js +0 -0
  476. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → frfHFnRYTA0Wjrw8oZJtY}/_ssgManifest.js +0 -0
  477. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → frfHFnRYTA0Wjrw8oZJtY}/_buildManifest.js +0 -0
  478. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → frfHFnRYTA0Wjrw8oZJtY}/_ssgManifest.js +0 -0
@@ -0,0 +1,685 @@
1
+ // The Python the scanner runs on, and where it comes from (docs/brain/DESKTOP.md D2).
2
+ //
3
+ // In this order:
4
+ // 1. The Mac app's own. A standalone CPython 3.12 with the scanner's packages
5
+ // already installed ships inside the app (Contents/Resources/python), and
6
+ // the app names it in SIX_DEGREES_PYTHON. Nothing to install, and it works
7
+ // offline.
8
+ // 2. The scanner's own environment in the data folder (venv/), which the Scan
9
+ // page's Install builds, or a Python on this computer that already has the
10
+ // packages (someone who set it up by hand).
11
+ // 3. For Install, something to build that environment from: a Python 3.10 to
12
+ // 3.14 on this computer, or else a standalone CPython in the data folder
13
+ // (python/), which the Scan page's "Set up the scanner" downloads when the
14
+ // computer has no Python the scanner can use: `npx six-degrees` on Linux,
15
+ // or on a Mac without the app. Only on that click, only the file pinned
16
+ // below for this computer, and it is checked against its SHA-256 before
17
+ // anything is unpacked. The packages then come from PyPI, pinned by hash
18
+ // in scripts/requirements.txt.
19
+ //
20
+ // scripts/build-app.mjs uses the same pins for the Python inside the app. What
21
+ // can be decided without a process or a network is here, so
22
+ // tests/scanner-python.test.mjs can check it offline.
23
+
24
+ import { spawn, execFileSync } from 'node:child_process';
25
+ import { createHash } from 'node:crypto';
26
+ import fs from 'node:fs';
27
+ import fsp from 'node:fs/promises';
28
+ import path from 'node:path';
29
+ import { chipOf } from './updater.js';
30
+ import { SETUP_WORK_PREFIX } from './data-folder.js';
31
+
32
+ /**
33
+ * The standalone CPython: python-build-standalone's release (a date), the
34
+ * CPython in it, and one file per computer, with its SHA-256 and size from that
35
+ * release's SHA256SUMS (checked against the files themselves on 2026-09-25).
36
+ * The Mac files are the "install_only" builds the app bundles. The Linux ones
37
+ * are "install_only_stripped": the same build without debug symbols, 33 MB to
38
+ * download instead of 104 (on a Mac, stripping saves 0.2 MB).
39
+ *
40
+ * To move to a newer one: take the new release's SHA256SUMS lines for these
41
+ * four names, and each file's size, and change all of them together.
42
+ */
43
+ export const STANDALONE_PYTHON = Object.freeze({
44
+ release: '20260814',
45
+ version: '3.12.14',
46
+ builds: Object.freeze({
47
+ 'darwin-arm64': Object.freeze({
48
+ file: 'cpython-3.12.14+20260814-aarch64-apple-darwin-install_only.tar.gz',
49
+ sha256: '4572133a5542f306b9bdb155da5800f9e38950cd0a98d469b832ce256fe299ea',
50
+ size: 25151480,
51
+ }),
52
+ 'darwin-x64': Object.freeze({
53
+ file: 'cpython-3.12.14+20260814-x86_64-apple-darwin-install_only.tar.gz',
54
+ sha256: '1a94c83264731e9603fbea78e57e7ca8f20e7d91eb866627ac2304621b0f6f1f',
55
+ size: 24839099,
56
+ }),
57
+ 'linux-x64': Object.freeze({
58
+ file: 'cpython-3.12.14+20260814-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz',
59
+ sha256: '5acfa3e9ba26b51ae161c83aff278da915b590d22373a424b2ba55b8afe91fcc',
60
+ size: 34143739,
61
+ }),
62
+ 'linux-arm64': Object.freeze({
63
+ file: 'cpython-3.12.14+20260814-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz',
64
+ sha256: '2d8e17dfd732102cfeb18e0e1fa6769b24caa034e159981129590fe409c7157a',
65
+ size: 29217771,
66
+ }),
67
+ }),
68
+ });
69
+
70
+ /** GitHub's release downloads for python-build-standalone: the one place the Python comes from. */
71
+ export const STANDALONE_BASE = 'https://github.com/astral-sh/python-build-standalone/releases/download';
72
+
73
+ /** The download address of a pinned build ("+" in its name is %2B, as GitHub lists it). */
74
+ export function standaloneUrl(build, pin = STANDALONE_PYTHON) {
75
+ return `${STANDALONE_BASE}/${pin.release}/${encodeURIComponent(build.file)}`;
76
+ }
77
+
78
+ /**
79
+ * This computer, as a key of STANDALONE_PYTHON.builds, or null when there is
80
+ * no build for it (Windows, 32-bit, other chips).
81
+ *
82
+ * On a Mac the chip is the hardware's (`hw.optional.arm64`, as install.sh and
83
+ * the updater ask), not this process's: Node running under Rosetta calls itself
84
+ * x64 on Apple Silicon (TRAPS §30), and the scanner would then run translated.
85
+ */
86
+ export function hostKey({ platform = process.platform, arch = process.arch, sysctlArm64 = '' } = {}) {
87
+ if (platform === 'darwin') return `darwin-${chipOf({ sysctlArm64, processArch: arch })}`;
88
+ if (platform === 'linux' && (arch === 'x64' || arch === 'arm64')) return `linux-${arch}`;
89
+ return null;
90
+ }
91
+
92
+ /** hostKey() for the computer this runs on. */
93
+ export function thisHostKey() {
94
+ let sysctlArm64 = '';
95
+ if (process.platform === 'darwin') {
96
+ try {
97
+ sysctlArm64 = execFileSync('/usr/sbin/sysctl', ['-n', 'hw.optional.arm64'],
98
+ { timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).toString();
99
+ } catch { /* an Intel Mac has no such key */ }
100
+ }
101
+ return hostKey({ sysctlArm64 });
102
+ }
103
+
104
+ /** The pinned build for a hostKey(), with its address, or null. */
105
+ export function standaloneBuild(key, pin = STANDALONE_PYTHON) {
106
+ const build = key && Object.hasOwn(pin.builds, key) ? pin.builds[key] : null;
107
+ return build ? { key, version: pin.version, release: pin.release, ...build, url: standaloneUrl(build, pin) } : null;
108
+ }
109
+
110
+ /**
111
+ * Which Python on this computer Install may build the scanner's environment
112
+ * from. The pinned packages (scripts/requirements.txt) have wheels for CPython
113
+ * 3.10 to 3.14 and nothing is built from source, so an older Python can't
114
+ * install them (Playwright needs 3.10) and a newer one has nothing to install.
115
+ */
116
+ export const SYSTEM_PYTHON = Object.freeze({ min: [3, 10], max: [3, 14] });
117
+
118
+ /**
119
+ * The names a Python on this computer goes by, in the order they are tried.
120
+ * /usr/bin/python3 first, as before: on a Mac it is often the one that already
121
+ * has the packages (TRAPS §14). The numbered names find a Homebrew or deadsnakes
122
+ * Python whose `python3` points elsewhere; a name that isn't there costs nothing.
123
+ */
124
+ export const SYSTEM_CANDIDATES = Object.freeze([
125
+ '/usr/bin/python3', 'python3', 'python',
126
+ 'python3.14', 'python3.13', 'python3.12', 'python3.11', 'python3.10',
127
+ ]);
128
+
129
+ /** Is "3.12.4" a Python the pinned packages install into? */
130
+ export function systemPythonFits(version, range = SYSTEM_PYTHON) {
131
+ const m = String(version || '').match(/^(\d+)\.(\d+)/);
132
+ if (!m) return false;
133
+ const v = [Number(m[1]), Number(m[2])];
134
+ const cmp = (a, b) => (a[0] - b[0]) || (a[1] - b[1]);
135
+ return cmp(v, range.min) >= 0 && cmp(v, range.max) <= 0;
136
+ }
137
+
138
+ // Load the compiled parts, not just the package names. `import PIL` succeeds even
139
+ // when its C extension is built for the other chip; `from PIL import Image` is
140
+ // what fails (TRAPS §30).
141
+ export const IMPORTS = 'import requests; from PIL import Image; from playwright.sync_api import sync_playwright';
142
+
143
+ /**
144
+ * One look at a Python: its version, whether it can make an environment
145
+ * (venv and ensurepip: Debian and Ubuntu leave them out until python3-venv is
146
+ * installed), and whether the scanner's packages load, with the reason when
147
+ * they don't. One process instead of one per question.
148
+ */
149
+ export const PROBE_SCRIPT = [
150
+ 'import sys',
151
+ 'print("version %d.%d.%d" % sys.version_info[:3])',
152
+ 'try:',
153
+ ' import venv, ensurepip',
154
+ ' print("venv")',
155
+ 'except Exception:',
156
+ ' pass',
157
+ 'try:',
158
+ ` ${IMPORTS}`,
159
+ ' print("imports")',
160
+ 'except Exception as e:',
161
+ ' print("missing %s: %s" % (type(e).__name__, (str(e).splitlines() or [""])[0][:200]))',
162
+ ].join('\n');
163
+
164
+ /** What PROBE_SCRIPT printed, as { version, venv, imports, missing }. */
165
+ export function parseProbe(stdout) {
166
+ const out = { version: null, venv: false, imports: false, missing: null };
167
+ for (const line of String(stdout || '').split('\n').map((l) => l.trim())) {
168
+ if (line.startsWith('version ')) out.version = line.slice(8).trim() || null;
169
+ else if (line === 'venv') out.venv = true;
170
+ else if (line === 'imports') out.imports = true;
171
+ else if (line.startsWith('missing ')) out.missing = line.slice(8).trim() || null;
172
+ }
173
+ return out;
174
+ }
175
+
176
+ /**
177
+ * How the app's own Python starts: the one inside the Mac app, one named in
178
+ * SIX_DEGREES_PYTHON, and the one Set up the scanner downloaded. It must use
179
+ * its own packages and nothing of this user's:
180
+ * -E no PYTHON* setting from this user's environment counts. Not PYTHONPATH
181
+ * or PYTHONHOME, and not PYTHONPLATLIBDIR, which alone stops Python
182
+ * before it starts, or PYTHONSAFEPATH, which hides scrape.py's own folder
183
+ * from its imports
184
+ * -s no user site-packages (a `pip install --user` of another Playwright
185
+ * would come first on the path)
186
+ * -B no bytecode: inside the app that would be writing into its own signed
187
+ * bundle, which breaks its signature
188
+ * -u unbuffered, which PYTHONUNBUFFERED said before -E: the Scan page shows
189
+ * the scanner's lines as they come
190
+ * Not -I: that also takes scrape.py's own folder off the import path, and with
191
+ * it `import image_store`.
192
+ */
193
+ export const OWN_PYTHON_FLAGS = Object.freeze(['-E', '-s', '-B', '-u']);
194
+
195
+ /**
196
+ * The environment the app's own Python runs with, beside OWN_PYTHON_FLAGS. The
197
+ * flags keep this user's PYTHON* settings from the interpreter; leaving them out
198
+ * of its environment keeps them from whatever it starts too (the Python that
199
+ * `-m venv` starts to put pip in place, for one).
200
+ */
201
+ export function ownPythonEnv(env = process.env) {
202
+ const out = {};
203
+ for (const [name, value] of Object.entries(env)) {
204
+ if (!name.startsWith('PYTHON')) out[name] = value;
205
+ }
206
+ out.PYTHONNOUSERSITE = '1';
207
+ out.PYTHONDONTWRITEBYTECODE = '1';
208
+ return out;
209
+ }
210
+
211
+ /**
212
+ * The environment a scan on this user's own Python gets (the scanner's
213
+ * environment in the data folder, or this computer's Python): theirs, as it
214
+ * always was, but no bytecode. scrape.py imports image_store from its own
215
+ * folder, so Python would write scripts/__pycache__ beside it, and in the Mac
216
+ * app that folder is inside the signed app, whichever Python runs the scan.
217
+ */
218
+ export function noBytecodeEnv(env = process.env) {
219
+ return { ...env, PYTHONDONTWRITEBYTECODE: '1' };
220
+ }
221
+
222
+ /** Is this Python the app's own (choosePython's 'bundled' or 'custom'), run isolated? */
223
+ export const isOwnPython = (run) => run?.source === 'bundled' || run?.source === 'custom';
224
+
225
+ /**
226
+ * How the scanner (scrape.py, at `script`) is started on the Python
227
+ * choosePython() picked: { cmd, args, env }, where env(base) makes its
228
+ * environment from the server's. The app's own Python runs isolated
229
+ * (OWN_PYTHON_FLAGS, ownPythonEnv); any other writes no bytecode (noBytecodeEnv).
230
+ */
231
+ export function scannerCommand(run, script, args = []) {
232
+ const own = isOwnPython(run);
233
+ return {
234
+ cmd: run.path,
235
+ args: [...(own ? OWN_PYTHON_FLAGS : []), script, ...args],
236
+ env: own ? ownPythonEnv : noBytecodeEnv,
237
+ };
238
+ }
239
+
240
+ // pip settings that send an install somewhere other than the environment pip
241
+ // runs in (--target, --prefix, --root: pip then says it succeeded, and the
242
+ // scanner's environment has nothing), or make it refuse there (--user). pip
243
+ // reads any PIP_<NAME>, whatever the case of NAME, with _ for -.
244
+ const PIP_ELSEWHERE = new Set(['target', 'prefix', 'root', 'user']);
245
+
246
+ /**
247
+ * The environment Install's pip runs with: this user's, less the settings that
248
+ * would send the packages somewhere else (PIP_ELSEWHERE). Everything else of
249
+ * theirs stays: which index to use, a proxy, certificates. Behind a company
250
+ * network those are what make pip work at all, and scripts/requirements.txt's
251
+ * hashes still have to match whatever serves the files.
252
+ */
253
+ export function pipInstallEnv(env = process.env) {
254
+ const out = {};
255
+ for (const [name, value] of Object.entries(env)) {
256
+ if (name.startsWith('PIP_') && PIP_ELSEWHERE.has(name.slice(4).toLowerCase().replace(/_/g, '-'))) continue;
257
+ out[name] = value;
258
+ }
259
+ return out;
260
+ }
261
+
262
+ /**
263
+ * Install's last step, run by the scanner's environment: do the packages load
264
+ * there? pip can say it succeeded after putting them somewhere else (a pip.conf
265
+ * with a target, say), and the page would then say Finished over a scanner that
266
+ * can't start. When they don't load it exits 1, with the reason in words.
267
+ * `imports` is for tests.
268
+ */
269
+ export function installCheckScript(imports = IMPORTS) {
270
+ return [
271
+ 'import sys',
272
+ 'try:',
273
+ ` ${imports}`,
274
+ 'except Exception as e:',
275
+ ' first = (str(e).splitlines() or [""])[0][:200]',
276
+ ' sys.stderr.write("pip said it installed them, but the scanner\'s environment can\'t load them (%s: %s). '
277
+ + 'A pip setting may send packages somewhere else: pip config list shows yours.\\n" % (type(e).__name__, first))',
278
+ ' sys.exit(1)',
279
+ 'print("The scanner\'s packages load in its environment.")',
280
+ ].join('\n');
281
+ }
282
+
283
+ /**
284
+ * Install's steps: build the scanner's environment from `base` (choosePython's
285
+ * base: this computer's Python, or the one Set up downloaded, which is the
286
+ * app's own and runs isolated), install the pinned packages into it, then
287
+ * check they load there. Each step is { cmd, args, note, env? }, where env(base)
288
+ * makes its environment from the server's.
289
+ */
290
+ export function installSteps({ base, dataDir, requirements }) {
291
+ const own = base.source === 'downloaded';
292
+ const python = venvPython(dataDir);
293
+ return [
294
+ // --clear: Install runs only when the environment doesn't work, so what is there goes.
295
+ {
296
+ cmd: base.path,
297
+ args: [...(own ? OWN_PYTHON_FLAGS : []), '-m', 'venv', '--clear', venvDir(dataDir)],
298
+ ...(own ? { env: ownPythonEnv } : {}),
299
+ note: 'Creating the scanner’s own Python environment',
300
+ },
301
+ // Every file pinned by its hash, wheels only (scripts/requirements.txt says so itself).
302
+ {
303
+ cmd: python,
304
+ args: ['-m', 'pip', 'install', '--disable-pip-version-check', '--no-input', '-r', requirements],
305
+ env: pipInstallEnv,
306
+ note: 'Installing Playwright, requests and Pillow',
307
+ },
308
+ { cmd: python, args: ['-c', installCheckScript()], note: 'Checking that they load' },
309
+ ];
310
+ }
311
+
312
+ /** The scanner's environment in the data folder, and the Python in it. */
313
+ export function venvDir(dataDir) {
314
+ return path.join(dataDir, 'venv');
315
+ }
316
+ export function venvPython(dataDir, platform = process.platform) {
317
+ return platform === 'win32'
318
+ ? path.join(venvDir(dataDir), 'Scripts', 'python.exe')
319
+ : path.join(venvDir(dataDir), 'bin', 'python');
320
+ }
321
+
322
+ /** Where "Set up the scanner" puts the standalone Python, and its interpreter. */
323
+ export const DOWNLOADED_DIR = 'python';
324
+ export function downloadedPython(dataDir) {
325
+ return path.join(dataDir, DOWNLOADED_DIR, 'bin', 'python3');
326
+ }
327
+
328
+ // Private working folders for a setup, inside the data folder (lib/data-folder.js
329
+ // keeps the names of every such folder): swept once an hour old, at a server's
330
+ // start as well as before a setup.
331
+ export { SETUP_WORK_PREFIX };
332
+
333
+ /**
334
+ * Run a Python briefly and report what happened: { ok, stdout, stderr, problem }.
335
+ * `problem` says why it didn't run, in a few words, when it didn't.
336
+ */
337
+ export function runProbe(cmd, args, { timeoutMs = 8000, env } = {}) {
338
+ return new Promise((resolve) => {
339
+ let stdout = '';
340
+ let stderr = '';
341
+ let done = false;
342
+ const finish = (value) => { if (!done) { done = true; resolve(value); } };
343
+ let child;
344
+ try {
345
+ child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], ...(env ? { env } : {}) });
346
+ } catch (err) {
347
+ finish({ ok: false, stdout, stderr, problem: err.message });
348
+ return;
349
+ }
350
+ const cap = (s, d) => (s.length < 16384 ? s + d.toString() : s);
351
+ child.stdout.on('data', (d) => { stdout = cap(stdout, d); });
352
+ child.stderr.on('data', (d) => { stderr = cap(stderr, d); });
353
+ const timer = setTimeout(() => {
354
+ try { child.kill('SIGKILL'); } catch { /* already gone */ }
355
+ finish({ ok: false, stdout, stderr, problem: `it didn't answer within ${Math.round(timeoutMs / 1000)} seconds` });
356
+ }, timeoutMs);
357
+ child.on('error', (err) => {
358
+ clearTimeout(timer);
359
+ finish({ ok: false, stdout, stderr, problem: err.code === 'ENOENT' ? 'it isn\'t there' : err.message });
360
+ });
361
+ child.on('close', (code, signal) => {
362
+ clearTimeout(timer);
363
+ const last = stderr.trim().split('\n').pop() || '';
364
+ finish({
365
+ ok: code === 0,
366
+ stdout,
367
+ stderr,
368
+ problem: code === 0 ? null : signal ? `it was stopped (${signal})` : (last || `it exited with ${code}`),
369
+ });
370
+ });
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Which Python runs the scanner, and what the Scan page can offer when none
376
+ * can. Looks in the order at the top of this file, and stops looking once it
377
+ * has an answer: when the app's own Python works, nothing on the computer is
378
+ * started at all (on a Mac without the developer tools, /usr/bin/python3 is a
379
+ * stub that asks to install them).
380
+ *
381
+ * bundled SIX_DEGREES_PYTHON, or '' when unset
382
+ * app SIX_DEGREES_APP, the running app bundle: a named Python
383
+ * inside it is the app's own ('bundled'), any other is 'custom'
384
+ * dataDir the data folder
385
+ * probe(cmd, args, { timeoutMs, env }) runProbe, or a stand-in in tests
386
+ * exists(path) fs.existsSync, or a stand-in
387
+ * host hostKey() of this computer
388
+ * systemCandidates the names a Python on this computer goes by
389
+ * env the environment Pythons are started from (process.env)
390
+ *
391
+ * Resolves to:
392
+ * run { path, source: 'bundled'|'custom'|'venv'|'system', version } or null
393
+ * own when the named Python (SIX_DEGREES_PYTHON) didn't do:
394
+ * { source: 'bundled'|'custom', problem }, else null
395
+ * base when nothing runs: what Install builds the environment from,
396
+ * { path, source: 'system'|'downloaded', version }, or null
397
+ * systemFound when there is no base: a Python this computer does have, as
398
+ * { version, venv }, so the page can say why it won't do
399
+ * download when there is neither: the pinned build for this computer
400
+ * (standaloneBuild), which "Set up the scanner" fetches, or null
401
+ */
402
+ export async function choosePython({
403
+ bundled = '',
404
+ app = '',
405
+ dataDir,
406
+ probe = runProbe,
407
+ exists = fs.existsSync,
408
+ host = null,
409
+ systemCandidates = SYSTEM_CANDIDATES,
410
+ timeouts = { own: 20000, other: 8000 },
411
+ env = process.env,
412
+ } = {}) {
413
+ const result = { run: null, own: null, base: null, systemFound: null, download: null };
414
+
415
+ if (bundled) {
416
+ const source = app && path.resolve(bundled).startsWith(`${path.resolve(app)}${path.sep}`) ? 'bundled' : 'custom';
417
+ // Asked the way it will run the scanner: isolated from this user's settings.
418
+ const r = await probe(bundled, [...OWN_PYTHON_FLAGS, '-c', PROBE_SCRIPT], { timeoutMs: timeouts.own, env: ownPythonEnv(env) });
419
+ const p = parseProbe(r.stdout);
420
+ if (r.ok && p.imports) {
421
+ result.run = { path: bundled, source, version: p.version };
422
+ return result;
423
+ }
424
+ result.own = { source, problem: p.missing || r.problem || 'it didn\'t start' };
425
+ }
426
+
427
+ // The scanner's own environment, built earlier by Install.
428
+ const venv = venvPython(dataDir);
429
+ if (exists(venv)) {
430
+ const r = await probe(venv, ['-c', PROBE_SCRIPT], { timeoutMs: timeouts.other, env });
431
+ const p = parseProbe(r.stdout);
432
+ if (r.ok && p.imports) {
433
+ result.run = { path: venv, source: 'venv', version: p.version };
434
+ return result;
435
+ }
436
+ }
437
+
438
+ // A Python on this computer: one that already has the packages is used as it
439
+ // is (someone who installed them by hand); otherwise the first that fits is
440
+ // what Install builds from.
441
+ const seen = [];
442
+ for (const cmd of systemCandidates) {
443
+ const r = await probe(cmd, ['-c', PROBE_SCRIPT], { timeoutMs: timeouts.other, env });
444
+ const p = parseProbe(r.stdout);
445
+ if (!p.version) continue;
446
+ if (r.ok && p.imports) {
447
+ result.run = { path: cmd, source: 'system', version: p.version };
448
+ return result;
449
+ }
450
+ seen.push({ path: cmd, version: p.version, venv: p.venv });
451
+ }
452
+ const fits = seen.find((s) => s.venv && systemPythonFits(s.version));
453
+ if (fits) {
454
+ result.base = { path: fits.path, source: 'system', version: fits.version };
455
+ return result;
456
+ }
457
+ // The most telling one first: the right version without venv, then any.
458
+ const near = seen.find((s) => systemPythonFits(s.version)) || seen[0];
459
+ result.systemFound = near ? { version: near.version, venv: near.venv } : null;
460
+
461
+ // The standalone Python "Set up the scanner" downloaded, if it is here.
462
+ const own = downloadedPython(dataDir);
463
+ if (exists(own)) {
464
+ const r = await probe(own, [...OWN_PYTHON_FLAGS, '-c', PROBE_SCRIPT], { timeoutMs: timeouts.other, env: ownPythonEnv(env) });
465
+ const p = parseProbe(r.stdout);
466
+ if (r.ok && p.venv) {
467
+ result.base = { path: own, source: 'downloaded', version: p.version };
468
+ return result;
469
+ }
470
+ }
471
+
472
+ result.download = standaloneBuild(host);
473
+ return result;
474
+ }
475
+
476
+ /**
477
+ * choosePython() for a server that asks again and again (the Scan page asks
478
+ * every 1.5 seconds), remembering how the Python it is told to use first
479
+ * (`named`, SIX_DEGREES_PYTHON) went:
480
+ * - once it has worked it isn't started again to ask: it can't change while
481
+ * the server runs;
482
+ * - the app's own ('bundled') that didn't work isn't started again while this
483
+ * server runs. If macOS refused to run it (say, Gatekeeper blocking the
484
+ * Python inside an app installed from a quarantined download), every start
485
+ * could bring its "python3.12 Not Opened" alert back. Its failure is
486
+ * reported on every look instead, with retry: false, so the Scan page says
487
+ * what happened and what to do (lib/scanner-setup.js); a restart asks again;
488
+ * - one named from outside the app ('custom') that didn't work is asked again
489
+ * a minute later: someone may be fixing it. A first start can take 20
490
+ * seconds to give up, and every look would wait on it.
491
+ * `options` go to choosePython as they are.
492
+ */
493
+ export function pythonLooker({ choose = choosePython, now = Date.now, retryMs = 60000 } = {}) {
494
+ let works = null; // the named Python's `run`, once it has worked
495
+ let failed = null; // { path, at, own }
496
+ return async function lookForPython(named, options = {}) {
497
+ if (named && works?.path === named) {
498
+ return { run: works, own: null, base: null, systemFound: null, download: null };
499
+ }
500
+ const knownBad = Boolean(named) && failed?.path === named
501
+ && (failed.own.retry === false || now() - failed.at < retryMs);
502
+ const python = await choose({ ...options, bundled: knownBad ? '' : named });
503
+ if (knownBad) return { ...python, own: failed.own };
504
+ if (named && python.run?.path === named) {
505
+ works = python.run;
506
+ } else if (named && python.own) {
507
+ const own = python.own.source === 'bundled' ? { ...python.own, retry: false } : python.own;
508
+ failed = { path: named, at: now(), own };
509
+ return { ...python, own };
510
+ }
511
+ return python;
512
+ };
513
+ }
514
+
515
+ /** A setup that can't go on, with a message fit to show on the Scan page as it is. */
516
+ export class ScannerSetupError extends Error {}
517
+
518
+ // Next wraps the global fetch to cache what pages fetch. A download is no page's
519
+ // business, so use the fetch underneath when Next says which one that is.
520
+ const plainFetch = (...args) => (globalThis.fetch?._nextOriginalFetch || globalThis.fetch)(...args);
521
+
522
+ const MB = 1024 * 1024;
523
+ export const megabytes = (bytes) => (bytes / MB).toFixed(1);
524
+
525
+ /**
526
+ * Download `url` to `file`, checking it against its pinned SHA-256 and size as
527
+ * it arrives. It is written to `<file>.part` and renamed to `file` only once
528
+ * both match, so nothing can unpack a file that failed the check: a wrong one
529
+ * is deleted, and the promise rejects with a ScannerSetupError. Gives up when
530
+ * nothing has arrived for `idleMs`; `signal` cancels it.
531
+ * `onProgress(received, total)` is called as it goes.
532
+ */
533
+ export async function downloadVerified(url, file, {
534
+ sha256, size, signal, onProgress, idleMs = 30000, fetchImpl = plainFetch, from = 'GitHub',
535
+ } = {}) {
536
+ if (!/^[0-9a-f]{64}$/.test(String(sha256)) || !(Number.isInteger(size) && size > 0)) {
537
+ throw new ScannerSetupError('There is no pinned checksum for this download, so it wasn\'t started.');
538
+ }
539
+ const part = `${file}.part`;
540
+ const idle = new AbortController();
541
+ const both = signal ? AbortSignal.any([signal, idle.signal]) : idle.signal;
542
+ let timer = null;
543
+ const arm = () => {
544
+ clearTimeout(timer);
545
+ timer = setTimeout(() => idle.abort(new ScannerSetupError(
546
+ `The download stopped for ${Math.round(idleMs / 1000)} seconds. Check your connection and try again.`,
547
+ )), idleMs);
548
+ };
549
+ arm();
550
+ const fh = await fsp.open(part, 'w');
551
+ const hash = createHash('sha256');
552
+ let received = 0;
553
+ let good = false;
554
+ try {
555
+ const res = await fetchImpl(url, {
556
+ signal: both,
557
+ redirect: 'follow',
558
+ cache: 'no-store',
559
+ headers: { Accept: 'application/octet-stream', 'User-Agent': 'six-degrees-scanner-setup' },
560
+ });
561
+ if (!res.ok || !res.body) throw new ScannerSetupError(`${from} answered ${res.status} to the download. Try again later.`);
562
+ const declared = Number(res.headers.get('content-length')) || null;
563
+ if (declared && declared !== size) {
564
+ throw new ScannerSetupError(`The download isn't the size it should be (${declared} bytes, not ${size}), so it was stopped.`);
565
+ }
566
+ for await (const chunk of res.body) {
567
+ arm();
568
+ received += chunk.byteLength;
569
+ if (received > size) throw new ScannerSetupError('The download is bigger than it should be, so it was stopped.');
570
+ hash.update(chunk);
571
+ let offset = 0;
572
+ while (offset < chunk.byteLength) {
573
+ const { bytesWritten } = await fh.write(chunk, offset, chunk.byteLength - offset);
574
+ offset += bytesWritten;
575
+ }
576
+ onProgress?.(received, size);
577
+ }
578
+ if (received !== size) throw new ScannerSetupError('The download ended early. Try again.');
579
+ const got = hash.digest('hex');
580
+ if (got !== sha256) {
581
+ throw new ScannerSetupError('The download doesn\'t match the checksum Six Degrees has for it, so it was deleted and nothing was unpacked.');
582
+ }
583
+ good = true;
584
+ return got;
585
+ } catch (err) {
586
+ // Say why it stopped (cancelled, or nothing arriving), not "aborted".
587
+ if (both.aborted && both.reason instanceof Error) throw both.reason;
588
+ throw err;
589
+ } finally {
590
+ clearTimeout(timer);
591
+ await fh.close();
592
+ if (good) await fsp.rename(part, file);
593
+ else await fsp.rm(part, { force: true });
594
+ }
595
+ }
596
+
597
+ /** Remove setup folders a stopped server left behind, once they are an hour old. */
598
+ export function sweepSetupLeftovers(dataDir, { now = Date.now(), maxAgeMs = 3600000 } = {}) {
599
+ let names = [];
600
+ try { names = fs.readdirSync(dataDir); } catch { return; }
601
+ for (const name of names) {
602
+ if (!name.startsWith(SETUP_WORK_PREFIX)) continue;
603
+ const dir = path.join(dataDir, name);
604
+ try {
605
+ const st = fs.lstatSync(dir);
606
+ if (st.isDirectory() && now - st.mtimeMs > maxAgeMs) fs.rmSync(dir, { recursive: true, force: true });
607
+ } catch { /* gone already */ }
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Put the unpacked standalone Python in its place in the data folder. `unpacked`
613
+ * is the folder the archive was unpacked into; it must hold python/bin/python3.
614
+ * A Python already there (one that didn't work, or setup wouldn't have run) is
615
+ * moved into `work` first and goes when `work` is removed.
616
+ */
617
+ export async function placeDownloadedPython(unpacked, dataDir, work) {
618
+ const from = path.join(unpacked, 'python');
619
+ const st = await fsp.lstat(path.join(from, 'bin', 'python3')).catch(() => null);
620
+ if (!st) throw new ScannerSetupError('The download didn\'t contain the Python it should have.');
621
+ const to = path.join(dataDir, DOWNLOADED_DIR);
622
+ if (fs.existsSync(to)) await fsp.rename(to, path.join(work, 'python-before'));
623
+ await fsp.rename(from, to);
624
+ return downloadedPython(dataDir);
625
+ }
626
+
627
+ // ── Mach-O, for checking the Python that goes inside the Mac app ─────────────
628
+
629
+ const LC_VERSION_MIN_MACOSX = 0x24;
630
+ const LC_BUILD_VERSION = 0x32;
631
+ const DYLIB_COMMANDS = new Set([0xc, 0x80000018, 0x8000001f, 0x20]); // load, weak, reexport, lazy
632
+
633
+ /**
634
+ * What one Mach-O (a thin file, or one slice of a universal one) says about
635
+ * itself, from its load commands: the oldest macOS it runs on ('13.5') and the
636
+ * libraries it loads. null when `buf` at `offset` isn't a 64- or 32-bit Mach-O.
637
+ */
638
+ export function machoSlice(buf, offset = 0) {
639
+ if (!buf || offset + 28 > buf.length) return null;
640
+ const magic = buf.readUInt32LE(offset);
641
+ if (magic !== 0xfeedfacf && magic !== 0xfeedface) return null;
642
+ const ncmds = buf.readUInt32LE(offset + 16);
643
+ let at = offset + (magic === 0xfeedfacf ? 32 : 28);
644
+ const version = (v) => `${v >>> 16}.${(v >>> 8) & 0xff}${v & 0xff ? `.${v & 0xff}` : ''}`;
645
+ const info = { minos: null, dylibs: [] };
646
+ for (let i = 0; i < ncmds && at + 8 <= buf.length; i++) {
647
+ const cmd = buf.readUInt32LE(at);
648
+ const size = buf.readUInt32LE(at + 4);
649
+ if (size < 8) break;
650
+ if (cmd === LC_BUILD_VERSION && at + 16 <= buf.length) info.minos = version(buf.readUInt32LE(at + 12));
651
+ else if (cmd === LC_VERSION_MIN_MACOSX && at + 12 <= buf.length && !info.minos) info.minos = version(buf.readUInt32LE(at + 8));
652
+ else if (DYLIB_COMMANDS.has(cmd) && at + 12 <= buf.length) {
653
+ const nameAt = at + buf.readUInt32LE(at + 8);
654
+ const end = buf.indexOf(0, nameAt);
655
+ if (nameAt < at + size && end > nameAt) info.dylibs.push(buf.toString('utf8', nameAt, Math.min(end, at + size)));
656
+ }
657
+ at += size;
658
+ }
659
+ return info;
660
+ }
661
+
662
+ /**
663
+ * The slices of a Mach-O file as [{ arch, offset }]: one at offset 0 for a thin
664
+ * file, one per chip for a universal one. [] when it isn't a Mach-O (a script,
665
+ * a Java class file, which begins with the same bytes as a universal binary).
666
+ */
667
+ export function machoSlices(buf) {
668
+ if (!buf || buf.length < 8) return [];
669
+ const CPU = { 0x01000007: 'x64', 0x0100000c: 'arm64' };
670
+ const le = buf.readUInt32LE(0);
671
+ if (le === 0xfeedfacf || le === 0xfeedface) return [{ arch: CPU[buf.readUInt32LE(4) >>> 0] || 'other', offset: 0 }];
672
+ const be = buf.readUInt32BE(0);
673
+ if (be !== 0xcafebabe && be !== 0xcafebabf) return [];
674
+ const count = buf.readUInt32BE(4);
675
+ if (count < 1 || count > 16) return [];
676
+ const wide = be === 0xcafebabf;
677
+ const slices = [];
678
+ for (let i = 0; i < count; i++) {
679
+ const at = 8 + i * (wide ? 32 : 20);
680
+ if (at + (wide ? 16 : 12) > buf.length) break;
681
+ const offset = wide ? Number(buf.readBigUInt64BE(at + 8)) : buf.readUInt32BE(at + 8);
682
+ slices.push({ arch: CPU[buf.readUInt32BE(at) >>> 0] || 'other', offset });
683
+ }
684
+ return slices;
685
+ }