six-degrees 0.2.0 → 0.2.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 (180) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +11 -11
  4. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  14. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
  15. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/import.html +1 -1
  38. package/.next/standalone/.next/server/app/import.rsc +13 -13
  39. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  40. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  41. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
  42. package/.next/standalone/.next/server/app/index.html +1 -1
  43. package/.next/standalone/.next/server/app/index.rsc +13 -13
  44. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
  45. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  46. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/launch.html +2 -2
  50. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  51. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  52. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  53. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
  54. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/paths.html +1 -1
  59. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  60. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  61. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  62. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
  63. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/profile.html +1 -1
  66. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  67. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  68. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  69. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
  70. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/queue.html +1 -1
  73. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  74. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  75. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  76. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
  77. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/setup.html +1 -1
  80. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  81. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  82. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  83. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/_1_fynia._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/_1djpk8t._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  100. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  101. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  102. package/.next/standalone/.next/server/pages/404.html +1 -1
  103. package/.next/standalone/.next/server/pages/500.html +1 -1
  104. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  105. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  106. package/.next/standalone/.next/static/chunks/0jk64vo17avsb.js +1 -0
  107. package/.next/standalone/.next/static/chunks/0s65dsrthylo6.js +2 -0
  108. package/.next/standalone/.next/static/chunks/0uyfdhi8dek85.js +16 -0
  109. package/.next/standalone/.next/static/chunks/{1zmpcmngg5c7-.js → 229haorf0f19b.js} +1 -1
  110. package/.next/{static/chunks/3ts-_4i6bfkh2.js → standalone/.next/static/chunks/2g54uafnwuakc.js} +1 -1
  111. package/.next/standalone/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
  112. package/.next/{static/chunks/3pfyuezyqd9ob.js → standalone/.next/static/chunks/2spwqs6b6dx13.js} +1 -1
  113. package/.next/{static/chunks/1odow92u6wxz0.js → standalone/.next/static/chunks/38dlepuubiox0.js} +1 -1
  114. package/.next/standalone/AGENTS.md +10 -0
  115. package/.next/standalone/CHANGELOG.md +41 -0
  116. package/.next/standalone/CONTRIBUTING.md +9 -0
  117. package/.next/standalone/README.md +49 -29
  118. package/.next/standalone/SECURITY.md +12 -9
  119. package/.next/standalone/app/api/scraper/route.js +4 -3
  120. package/.next/standalone/app/api/update/route.js +10 -2
  121. package/.next/standalone/app/components/ForceGraph.js +1 -1
  122. package/.next/standalone/app/components/Sidebar.js +8 -8
  123. package/.next/standalone/app/components/UpdatePanel.js +39 -8
  124. package/.next/standalone/app/import/page.js +3 -3
  125. package/.next/standalone/app/layout.js +2 -2
  126. package/.next/standalone/app/page.js +2 -2
  127. package/.next/standalone/app/paths/page.js +1 -1
  128. package/.next/standalone/app/profile/page.js +0 -178
  129. package/.next/standalone/app/setup/page.js +4 -4
  130. package/.next/standalone/bin/six-degrees.mjs +7 -2
  131. package/.next/standalone/install.sh +1 -1
  132. package/.next/standalone/lib/csv.js +53 -27
  133. package/.next/standalone/lib/release.js +5 -2
  134. package/.next/standalone/lib/scraper-client.js +3 -3
  135. package/.next/standalone/package-lock.json +103 -9
  136. package/.next/standalone/package.json +12 -10
  137. package/.next/standalone/public/demo-data.json +1 -1
  138. package/.next/standalone/scripts/gen-synthetic.mjs +16 -1
  139. package/.next/standalone/scripts/readme_buttons.py +89 -0
  140. package/.next/standalone/scripts/screenshots.py +2 -0
  141. package/.next/standalone/site/app.js +84 -0
  142. package/.next/standalone/site/index.html +221 -0
  143. package/.next/standalone/site/style.css +155 -0
  144. package/.next/standalone/site/video/install-mac-poster.jpg +0 -0
  145. package/.next/standalone/site/video/install-mac.mp4 +0 -0
  146. package/.next/standalone/site/video/linux-poster.jpg +0 -0
  147. package/.next/standalone/site/video/linux.mp4 +0 -0
  148. package/.next/standalone/site/video/scan-poster.jpg +0 -0
  149. package/.next/standalone/site/video/scan.mp4 +0 -0
  150. package/.next/static/chunks/0jk64vo17avsb.js +1 -0
  151. package/.next/static/chunks/0s65dsrthylo6.js +2 -0
  152. package/.next/static/chunks/0uyfdhi8dek85.js +16 -0
  153. package/.next/static/chunks/{1zmpcmngg5c7-.js → 229haorf0f19b.js} +1 -1
  154. package/.next/{standalone/.next/static/chunks/3ts-_4i6bfkh2.js → static/chunks/2g54uafnwuakc.js} +1 -1
  155. package/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
  156. package/.next/{standalone/.next/static/chunks/3pfyuezyqd9ob.js → static/chunks/2spwqs6b6dx13.js} +1 -1
  157. package/.next/{standalone/.next/static/chunks/1odow92u6wxz0.js → static/chunks/38dlepuubiox0.js} +1 -1
  158. package/CHANGELOG.md +41 -0
  159. package/README.md +49 -29
  160. package/SECURITY.md +12 -9
  161. package/bin/six-degrees.mjs +7 -2
  162. package/lib/csv.js +53 -27
  163. package/lib/release.js +5 -2
  164. package/lib/scraper-client.js +3 -3
  165. package/package.json +12 -10
  166. package/public/demo-data.json +1 -1
  167. package/.next/standalone/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
  168. package/.next/standalone/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
  169. package/.next/standalone/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
  170. package/.next/standalone/.next/static/chunks/3hsi90cejq2dg.js +0 -1
  171. package/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
  172. package/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
  173. package/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
  174. package/.next/static/chunks/3hsi90cejq2dg.js +0 -1
  175. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_buildManifest.js +0 -0
  176. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_clientMiddlewareManifest.js +0 -0
  177. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_ssgManifest.js +0 -0
  178. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_buildManifest.js +0 -0
  179. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_clientMiddlewareManifest.js +0 -0
  180. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,2776,e=>{"use strict";var t=e.i(43476),r=e.i(15812);e.s(["default",0,function({children:e}){let{userId:n,ready:o}=(0,r.useUser)();return o&&n?e:(0,t.jsx)("div",{style:{minHeight:"100vh",display:"flex",alignItems:"center",justifyContent:"center",background:"#0a0a1a",color:"#fff",padding:24,textAlign:"center",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'},children:o?(0,t.jsxs)("div",{style:{maxWidth:420},children:[(0,t.jsx)("div",{style:{fontSize:18,fontWeight:700,marginBottom:8},children:"Can’t reach 6 Degrees’ local server"}),(0,t.jsx)("div",{style:{color:"#888",fontSize:14,lineHeight:1.6},children:"The page loaded but the app behind it is not answering. If you started it from a terminal, check that window for an error, then reload this page."})]}):(0,t.jsx)("div",{style:{fontSize:18,fontWeight:600},children:"Loading…"})})}])},19821,e=>{"use strict";async function t(e){let t=e?`/api/network?userId=${encodeURIComponent(e)}`:"/api/network",r=await fetch(t);if(!r.ok)throw Error(`Failed to load network (${r.status})`);let n=await r.json();return{degree1:n.degree1||[],degree2:n.degree2||[],degree3:n.degree3||[]}}e.s(["loadNetwork",0,t])},40996,e=>{"use strict";async function t(){let e=await fetch("/api/scraper");if(!e.ok)throw Error("Could not reach the app.");return e.json()}async function r(e,t,n={}){let o=await fetch("/api/scraper",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,...t?{name:t}:{},...n})}),a=await o.json().catch(()=>({}));if(!o.ok)throw Error(a.error||"Could not start the scraper.");return a}async function n(){await fetch("/api/scraper",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"cancel"})})}e.s(["notReadyMessage",0,function(e){return e?e.checks?.scriptsFound?e.checks?.python?e.checks?.dependencies?e.checks?.chrome?null:"Google Chrome is not installed.":"The scraper is not installed yet — open Scan to set it up.":"Python 3 is not installed on this machine.":"The scraper files are missing from this install.":"Could not reach the app."},"runScrape",0,function(e,{name:n,onLog:o,...a}={}){return new Promise((i,u)=>{r(e,n,a).then(()=>{let e=setInterval(async()=>{try{let r=await t();o?.(r.log||[]),r.running||(clearInterval(e),i(r))}catch{}},1500)}).catch(u)})},"scraperStatus",0,t,"startScrape",0,r,"stopScrape",0,n])},5258,e=>{"use strict";e.s(["TIER_COLORS",0,{S:"#f6c344",A:"#a78bfa",B:"#60a5fa",C:"#94a3b8",D:"#6b7280"},"TIER_COLORS_CLASSIC",0,{S:"#FFD700",A:"#9B59B6",B:"#3498DB",C:"#95A5A6",D:"#BDC3C7"},"TIER_ORDER",0,["S","A","B","C","D"],"TIER_RING_RADIUS",0,{S:150,A:230,B:310,C:390,D:470},"initialsFor",0,function(e){let t=String(e||"").trim().split(/\s+/).filter(Boolean);return 0===t.length?"?":(t[0].charAt(0)+(t.length>1?t[t.length-1].charAt(0):"")).toUpperCase()}])},22016,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={default:function(){return m},useLinkStatus:function(){return S}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=e.r(90809),i=e.r(43476),u=a._(e.r(71645)),s=e.r(95057),l=e.r(8372),c=e.r(18581),f=e.r(18967),p=e.r(5550),d=e.r(88540),h=e.r(91949),y=e.r(73668),g=e.r(9396);function m(t){var r;let n,o,a,[m,S]=(0,u.useOptimistic)(h.IDLE_LINK_STATUS),v=(0,u.useRef)(null),{href:C,as:P,children:E,prefetch:O=null,passHref:_,replace:j,shallow:T,scroll:R,onClick:w,onMouseEnter:A,onTouchStart:I,legacyBehavior:k=!1,onNavigate:x,transitionTypes:N,ref:L,unstable_dynamicOnHover:U,...B}=t;n=E,k&&("string"==typeof n||"number"==typeof n)&&(n=(0,i.jsx)("a",{children:n}));let D=u.default.useContext(l.AppRouterContext),M=!1!==O,$=!1===O?"none":!0===O?"full":"auto",F="none"!==$?"auto"===$?g.FetchStrategy.PPR:g.FetchStrategy.Full:g.FetchStrategy.PPR,z="string"==typeof(r=P||C)?r:(0,s.formatUrl)(r);if(k){if(n?.$$typeof===Symbol.for("react.lazy"))throw Object.defineProperty(Error("`<Link legacyBehavior>` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's `<a>` tag."),"__NEXT_ERROR_CODE",{value:"E863",enumerable:!1,configurable:!0});o=u.default.Children.only(n)}let K=k?o&&"object"==typeof o&&o.ref:L,W,J=u.default.useCallback(e=>(null!==D&&(v.current=(0,h.mountLinkInstance)(e,z,D,F,M,S,W)),()=>{v.current&&((0,h.unmountLinkForCurrentNavigation)(v.current),v.current=null),(0,h.unmountPrefetchableInstance)(e)}),[M,z,D,F,S,W]),G={ref:(0,c.useMergedRef)(J,K),onClick(t){k||"function"!=typeof w||w(t),k&&o.props&&"function"==typeof o.props.onClick&&o.props.onClick(t),!D||t.defaultPrevented||function(t,r,n,o,a,i,s,l="none"){if("u">typeof window){let c,{nodeName:f}=t.currentTarget;if("A"===f.toUpperCase()&&((c=t.currentTarget.getAttribute("target"))&&"_self"!==c||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.nativeEvent&&2===t.nativeEvent.which)||t.currentTarget.hasAttribute("download"))return;if(!(0,y.isLocalURL)(r)){o&&(t.preventDefault(),location.replace(r));return}if(t.preventDefault(),i){let e=!1;if(i({preventDefault:()=>{e=!0}}),e)return}let{dispatchNavigateAction:p}=e.r(99781);u.default.startTransition(()=>{p(r,o?"replace":"push",!1===a?d.ScrollBehavior.NoScroll:d.ScrollBehavior.Default,n.current,s,l)})}}(t,z,v,j,R,x,N,$)},onMouseEnter(e){k||"function"!=typeof A||A(e),k&&o.props&&"function"==typeof o.props.onMouseEnter&&o.props.onMouseEnter(e),D&&M&&(0,h.onNavigationIntent)(e.currentTarget,!0===U)},onTouchStart:function(e){k||"function"!=typeof I||I(e),k&&o.props&&"function"==typeof o.props.onTouchStart&&o.props.onTouchStart(e),D&&M&&(0,h.onNavigationIntent)(e.currentTarget,!0===U)}};return(0,f.isAbsoluteUrl)(z)?G.href=z:k&&!_&&("a"!==o.type||"href"in o.props)||(G.href=(0,p.addBasePath)(z)),a=k?u.default.cloneElement(o,G):(0,i.jsx)("a",{...B,...G,children:n}),(0,i.jsx)(b.Provider,{value:m,children:a})}let b=(0,u.createContext)(h.IDLE_LINK_STATUS),S=()=>(0,u.useContext)(b);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},18581,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"useMergedRef",{enumerable:!0,get:function(){return o}});let n=e.r(71645);function o(e,t){let r=(0,n.useRef)(null),o=(0,n.useRef)(null);return(0,n.useCallback)(n=>{if(null===n){let e=r.current;e&&(r.current=null,e());let t=o.current;t&&(o.current=null,t())}else e&&(r.current=a(e,n)),t&&(o.current=a(t,n))},[e,t])}function a(e,t){if("function"!=typeof e)return e.current=t,()=>{e.current=null};{let r=e(t);return"function"==typeof r?r:()=>e(null)}}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},18967,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={DecodeError:function(){return m},MiddlewareNotFoundError:function(){return C},MissingStaticPage:function(){return v},NormalizeError:function(){return b},PageNotFoundError:function(){return S},SP:function(){return y},ST:function(){return g},WEB_VITALS:function(){return a},execOnce:function(){return i},getDisplayName:function(){return f},getLocationOrigin:function(){return l},getURL:function(){return c},isAbsoluteUrl:function(){return s},isResSent:function(){return p},loadGetInitialProps:function(){return h},normalizeRepeatedSlashes:function(){return d},stringifyError:function(){return P}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=["CLS","FCP","FID","INP","LCP","TTFB"];function i(e){let t,r=!1;return(...n)=>(r||(r=!0,t=e(...n)),t)}let u=/^[a-zA-Z][a-zA-Z\d+\-.]*?:/,s=e=>{let t=e.charCodeAt(0);return!!(t>=65&&t<=90||t>=97&&t<=122)&&u.test(e)};function l(){let{protocol:e,hostname:t,port:r}=window.location;return`${e}//${t}${r?":"+r:""}`}function c(){let{href:e}=window.location,t=l();return e.substring(t.length)}function f(e){return"string"==typeof e?e:e.displayName||e.name||"Unknown"}function p(e){return e.finished||e.headersSent}function d(e){let t=e.split("?");return t[0].replace(/\\/g,"/").replace(/\/\/+/g,"/")+(t[1]?`?${t.slice(1).join("?")}`:"")}async function h(e,t){let r=t.res||t.ctx&&t.ctx.res;if(!e.getInitialProps)return t.ctx&&t.Component?{pageProps:await h(t.Component,t.ctx)}:{};let n=await e.getInitialProps(t);if(r&&p(r))return n;if(!n)throw Object.defineProperty(Error(`"${f(e)}.getInitialProps()" should resolve to an object. But found "${n}" instead.`),"__NEXT_ERROR_CODE",{value:"E1025",enumerable:!1,configurable:!0});return n}let y="u">typeof performance,g=y&&["mark","measure","getEntriesByName"].every(e=>"function"==typeof performance[e]);class m extends Error{}class b extends Error{}class S extends Error{constructor(e){super(),this.code="ENOENT",this.name="PageNotFoundError",this.message=`Cannot find module for page: ${e}`}}class v extends Error{constructor(e,t){super(),this.message=`Failed to load static file for page: ${e} ${t}`}}class C extends Error{constructor(){super(),this.code="ENOENT",this.message="Cannot find the middleware module"}}function P(e){return JSON.stringify({message:e.message,stack:e.stack})}},73668,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"isLocalURL",{enumerable:!0,get:function(){return a}});let n=e.r(18967),o=e.r(52817);function a(e){if(!(0,n.isAbsoluteUrl)(e))return!0;try{let t=(0,n.getLocationOrigin)(),r=new URL(e,t);return r.origin===t&&(0,o.hasBasePath)(r.pathname)}catch(e){return!1}}},98183,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={assign:function(){return s},searchParamsToUrlQuery:function(){return a},urlQueryToSearchParams:function(){return u}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});function a(e){let t={};for(let[r,n]of e.entries()){let e=t[r];void 0===e?t[r]=n:Array.isArray(e)?e.push(n):t[r]=[e,n]}return t}function i(e){return"string"==typeof e?e:("number"!=typeof e||isNaN(e))&&"boolean"!=typeof e?"":String(e)}function u(e){let t=new URLSearchParams;for(let[r,n]of Object.entries(e))if(Array.isArray(n))for(let e of n)t.append(r,i(e));else t.set(r,i(n));return t}function s(e,...t){for(let r of t){for(let t of r.keys())e.delete(t);for(let[t,n]of r.entries())e.append(t,n)}return e}},95057,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={formatUrl:function(){return u},formatWithValidation:function(){return l},urlObjectKeys:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=e.r(90809)._(e.r(98183)),i=/https?|ftp|gopher|file/;function u(e){let{auth:t,hostname:r}=e,n=e.protocol||"",o=e.pathname||"",u=e.hash||"",s=e.query||"",l=!1;t=t?encodeURIComponent(t).replace(/%3A/i,":")+"@":"",e.host?l=t+e.host:r&&(l=t+(~r.indexOf(":")?`[${r}]`:r),e.port&&(l+=":"+e.port)),s&&"object"==typeof s&&(s=String(a.urlQueryToSearchParams(s)));let c=e.search||s&&`?${s}`||"";return n&&!n.endsWith(":")&&(n+=":"),e.slashes||(!n||i.test(n))&&!1!==l?(l="//"+(l||""),o&&"/"!==o[0]&&(o="/"+o)):l||(l=""),u&&"#"!==u[0]&&(u="#"+u),c&&"?"!==c[0]&&(c="?"+c),o=o.replace(/[?#]/g,encodeURIComponent),c=c.replace("#","%23"),`${n}${l}${o}${c}${u}`}let s=["auth","hash","host","hostname","href","path","pathname","port","protocol","query","search","slashes"];function l(e){return u(e)}}]);
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,2776,e=>{"use strict";var t=e.i(43476),r=e.i(15812);e.s(["default",0,function({children:e}){let{userId:n,ready:o}=(0,r.useUser)();return o&&n?e:(0,t.jsx)("div",{style:{minHeight:"100vh",display:"flex",alignItems:"center",justifyContent:"center",background:"#0a0a1a",color:"#fff",padding:24,textAlign:"center",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'},children:o?(0,t.jsxs)("div",{style:{maxWidth:420},children:[(0,t.jsx)("div",{style:{fontSize:18,fontWeight:700,marginBottom:8},children:"Can’t reach 6 Degrees’ local server"}),(0,t.jsx)("div",{style:{color:"#888",fontSize:14,lineHeight:1.6},children:"The page loaded but the app behind it is not answering. If you started it from a terminal, check that window for an error, then reload this page."})]}):(0,t.jsx)("div",{style:{fontSize:18,fontWeight:600},children:"Loading…"})})}])},19821,e=>{"use strict";async function t(e){let t=e?`/api/network?userId=${encodeURIComponent(e)}`:"/api/network",r=await fetch(t);if(!r.ok)throw Error(`Failed to load network (${r.status})`);let n=await r.json();return{degree1:n.degree1||[],degree2:n.degree2||[],degree3:n.degree3||[]}}e.s(["loadNetwork",0,t])},40996,e=>{"use strict";async function t(){let e=await fetch("/api/scraper");if(!e.ok)throw Error("Could not reach the app.");return e.json()}async function r(e,t,n={}){let o=await fetch("/api/scraper",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,...t?{name:t}:{},...n})}),a=await o.json().catch(()=>({}));if(!o.ok)throw Error(a.error||"Could not start the scanner.");return a}async function n(){await fetch("/api/scraper",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"cancel"})})}e.s(["notReadyMessage",0,function(e){return e?e.checks?.scriptsFound?e.checks?.python?e.checks?.dependencies?e.checks?.chrome?null:"Google Chrome is not installed.":"The scanner is not installed yet — open Scan to set it up.":"Python 3 is not installed on this machine.":"The scanner files are missing from this install.":"Could not reach the app."},"runScrape",0,function(e,{name:n,onLog:o,...a}={}){return new Promise((i,u)=>{r(e,n,a).then(()=>{let e=setInterval(async()=>{try{let r=await t();o?.(r.log||[]),r.running||(clearInterval(e),i(r))}catch{}},1500)}).catch(u)})},"scraperStatus",0,t,"startScrape",0,r,"stopScrape",0,n])},5258,e=>{"use strict";e.s(["TIER_COLORS",0,{S:"#f6c344",A:"#a78bfa",B:"#60a5fa",C:"#94a3b8",D:"#6b7280"},"TIER_COLORS_CLASSIC",0,{S:"#FFD700",A:"#9B59B6",B:"#3498DB",C:"#95A5A6",D:"#BDC3C7"},"TIER_ORDER",0,["S","A","B","C","D"],"TIER_RING_RADIUS",0,{S:150,A:230,B:310,C:390,D:470},"initialsFor",0,function(e){let t=String(e||"").trim().split(/\s+/).filter(Boolean);return 0===t.length?"?":(t[0].charAt(0)+(t.length>1?t[t.length-1].charAt(0):"")).toUpperCase()}])},22016,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={default:function(){return m},useLinkStatus:function(){return S}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=e.r(90809),i=e.r(43476),u=a._(e.r(71645)),s=e.r(95057),l=e.r(8372),c=e.r(18581),f=e.r(18967),d=e.r(5550),p=e.r(88540),h=e.r(91949),y=e.r(73668),g=e.r(9396);function m(t){var r;let n,o,a,[m,S]=(0,u.useOptimistic)(h.IDLE_LINK_STATUS),v=(0,u.useRef)(null),{href:C,as:P,children:E,prefetch:O=null,passHref:_,replace:j,shallow:T,scroll:R,onClick:w,onMouseEnter:A,onTouchStart:I,legacyBehavior:k=!1,onNavigate:x,transitionTypes:N,ref:L,unstable_dynamicOnHover:U,...B}=t;n=E,k&&("string"==typeof n||"number"==typeof n)&&(n=(0,i.jsx)("a",{children:n}));let D=u.default.useContext(l.AppRouterContext),M=!1!==O,$=!1===O?"none":!0===O?"full":"auto",F="none"!==$?"auto"===$?g.FetchStrategy.PPR:g.FetchStrategy.Full:g.FetchStrategy.PPR,z="string"==typeof(r=P||C)?r:(0,s.formatUrl)(r);if(k){if(n?.$$typeof===Symbol.for("react.lazy"))throw Object.defineProperty(Error("`<Link legacyBehavior>` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's `<a>` tag."),"__NEXT_ERROR_CODE",{value:"E863",enumerable:!1,configurable:!0});o=u.default.Children.only(n)}let K=k?o&&"object"==typeof o&&o.ref:L,W,J=u.default.useCallback(e=>(null!==D&&(v.current=(0,h.mountLinkInstance)(e,z,D,F,M,S,W)),()=>{v.current&&((0,h.unmountLinkForCurrentNavigation)(v.current),v.current=null),(0,h.unmountPrefetchableInstance)(e)}),[M,z,D,F,S,W]),G={ref:(0,c.useMergedRef)(J,K),onClick(t){k||"function"!=typeof w||w(t),k&&o.props&&"function"==typeof o.props.onClick&&o.props.onClick(t),!D||t.defaultPrevented||function(t,r,n,o,a,i,s,l="none"){if("u">typeof window){let c,{nodeName:f}=t.currentTarget;if("A"===f.toUpperCase()&&((c=t.currentTarget.getAttribute("target"))&&"_self"!==c||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.nativeEvent&&2===t.nativeEvent.which)||t.currentTarget.hasAttribute("download"))return;if(!(0,y.isLocalURL)(r)){o&&(t.preventDefault(),location.replace(r));return}if(t.preventDefault(),i){let e=!1;if(i({preventDefault:()=>{e=!0}}),e)return}let{dispatchNavigateAction:d}=e.r(99781);u.default.startTransition(()=>{d(r,o?"replace":"push",!1===a?p.ScrollBehavior.NoScroll:p.ScrollBehavior.Default,n.current,s,l)})}}(t,z,v,j,R,x,N,$)},onMouseEnter(e){k||"function"!=typeof A||A(e),k&&o.props&&"function"==typeof o.props.onMouseEnter&&o.props.onMouseEnter(e),D&&M&&(0,h.onNavigationIntent)(e.currentTarget,!0===U)},onTouchStart:function(e){k||"function"!=typeof I||I(e),k&&o.props&&"function"==typeof o.props.onTouchStart&&o.props.onTouchStart(e),D&&M&&(0,h.onNavigationIntent)(e.currentTarget,!0===U)}};return(0,f.isAbsoluteUrl)(z)?G.href=z:k&&!_&&("a"!==o.type||"href"in o.props)||(G.href=(0,d.addBasePath)(z)),a=k?u.default.cloneElement(o,G):(0,i.jsx)("a",{...B,...G,children:n}),(0,i.jsx)(b.Provider,{value:m,children:a})}let b=(0,u.createContext)(h.IDLE_LINK_STATUS),S=()=>(0,u.useContext)(b);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},18581,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"useMergedRef",{enumerable:!0,get:function(){return o}});let n=e.r(71645);function o(e,t){let r=(0,n.useRef)(null),o=(0,n.useRef)(null);return(0,n.useCallback)(n=>{if(null===n){let e=r.current;e&&(r.current=null,e());let t=o.current;t&&(o.current=null,t())}else e&&(r.current=a(e,n)),t&&(o.current=a(t,n))},[e,t])}function a(e,t){if("function"!=typeof e)return e.current=t,()=>{e.current=null};{let r=e(t);return"function"==typeof r?r:()=>e(null)}}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},18967,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={DecodeError:function(){return m},MiddlewareNotFoundError:function(){return C},MissingStaticPage:function(){return v},NormalizeError:function(){return b},PageNotFoundError:function(){return S},SP:function(){return y},ST:function(){return g},WEB_VITALS:function(){return a},execOnce:function(){return i},getDisplayName:function(){return f},getLocationOrigin:function(){return l},getURL:function(){return c},isAbsoluteUrl:function(){return s},isResSent:function(){return d},loadGetInitialProps:function(){return h},normalizeRepeatedSlashes:function(){return p},stringifyError:function(){return P}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=["CLS","FCP","FID","INP","LCP","TTFB"];function i(e){let t,r=!1;return(...n)=>(r||(r=!0,t=e(...n)),t)}let u=/^[a-zA-Z][a-zA-Z\d+\-.]*?:/,s=e=>{let t=e.charCodeAt(0);return!!(t>=65&&t<=90||t>=97&&t<=122)&&u.test(e)};function l(){let{protocol:e,hostname:t,port:r}=window.location;return`${e}//${t}${r?":"+r:""}`}function c(){let{href:e}=window.location,t=l();return e.substring(t.length)}function f(e){return"string"==typeof e?e:e.displayName||e.name||"Unknown"}function d(e){return e.finished||e.headersSent}function p(e){let t=e.split("?");return t[0].replace(/\\/g,"/").replace(/\/\/+/g,"/")+(t[1]?`?${t.slice(1).join("?")}`:"")}async function h(e,t){let r=t.res||t.ctx&&t.ctx.res;if(!e.getInitialProps)return t.ctx&&t.Component?{pageProps:await h(t.Component,t.ctx)}:{};let n=await e.getInitialProps(t);if(r&&d(r))return n;if(!n)throw Object.defineProperty(Error(`"${f(e)}.getInitialProps()" should resolve to an object. But found "${n}" instead.`),"__NEXT_ERROR_CODE",{value:"E1025",enumerable:!1,configurable:!0});return n}let y="u">typeof performance,g=y&&["mark","measure","getEntriesByName"].every(e=>"function"==typeof performance[e]);class m extends Error{}class b extends Error{}class S extends Error{constructor(e){super(),this.code="ENOENT",this.name="PageNotFoundError",this.message=`Cannot find module for page: ${e}`}}class v extends Error{constructor(e,t){super(),this.message=`Failed to load static file for page: ${e} ${t}`}}class C extends Error{constructor(){super(),this.code="ENOENT",this.message="Cannot find the middleware module"}}function P(e){return JSON.stringify({message:e.message,stack:e.stack})}},73668,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"isLocalURL",{enumerable:!0,get:function(){return a}});let n=e.r(18967),o=e.r(52817);function a(e){if(!(0,n.isAbsoluteUrl)(e))return!0;try{let t=(0,n.getLocationOrigin)(),r=new URL(e,t);return r.origin===t&&(0,o.hasBasePath)(r.pathname)}catch(e){return!1}}},98183,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={assign:function(){return s},searchParamsToUrlQuery:function(){return a},urlQueryToSearchParams:function(){return u}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});function a(e){let t={};for(let[r,n]of e.entries()){let e=t[r];void 0===e?t[r]=n:Array.isArray(e)?e.push(n):t[r]=[e,n]}return t}function i(e){return"string"==typeof e?e:("number"!=typeof e||isNaN(e))&&"boolean"!=typeof e?"":String(e)}function u(e){let t=new URLSearchParams;for(let[r,n]of Object.entries(e))if(Array.isArray(n))for(let e of n)t.append(r,i(e));else t.set(r,i(n));return t}function s(e,...t){for(let r of t){for(let t of r.keys())e.delete(t);for(let[t,n]of r.entries())e.append(t,n)}return e}},95057,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={formatUrl:function(){return u},formatWithValidation:function(){return l},urlObjectKeys:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=e.r(90809)._(e.r(98183)),i=/https?|ftp|gopher|file/;function u(e){let{auth:t,hostname:r}=e,n=e.protocol||"",o=e.pathname||"",u=e.hash||"",s=e.query||"",l=!1;t=t?encodeURIComponent(t).replace(/%3A/i,":")+"@":"",e.host?l=t+e.host:r&&(l=t+(~r.indexOf(":")?`[${r}]`:r),e.port&&(l+=":"+e.port)),s&&"object"==typeof s&&(s=String(a.urlQueryToSearchParams(s)));let c=e.search||s&&`?${s}`||"";return n&&!n.endsWith(":")&&(n+=":"),e.slashes||(!n||i.test(n))&&!1!==l?(l="//"+(l||""),o&&"/"!==o[0]&&(o="/"+o)):l||(l=""),u&&"#"!==u[0]&&(u="#"+u),c&&"?"!==c[0]&&(c="?"+c),o=o.replace(/[?#]/g,encodeURIComponent),c=c.replace("#","%23"),`${n}${l}${o}${c}${u}`}let s=["auth","hash","host","hostname","href","path","pathname","port","protocol","query","search","slashes"];function l(e){return u(e)}}]);
package/CHANGELOG.md CHANGED
@@ -6,6 +6,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-09-25
10
+
11
+ ### Added
12
+ - **Tutorial videos on the download website:** installing on a Mac, running it on Linux, and
13
+ how scanning works. They're short and silent, with the steps as captions.
14
+ - **`npx six-degrees` on Linux.** The npm package is published (0.2.0 was the first). The
15
+ README, the website and the installer's message now point Linux users to it, with how to
16
+ update it and where it keeps your data.
17
+
18
+ ### Fixed
19
+ - **Check for updates says what it found.** An up-to-date copy showed only a faint line, so
20
+ the button seemed to do nothing. The answer is now a clear status ("You're up to date: 0.2.1
21
+ is the newest version", with the time). The Mac app's **Check for Updates…** menu item opens
22
+ the Updates panel and runs the check, instead of just opening the top of the Scan page.
23
+ - **The profile page no longer starts scans.** Its "Set Up Account — Full Scan" and "Auto-Bridge
24
+ All Connections" cards are gone. The second mapped every connection in one go, with none of
25
+ the batch size, budget or warnings of the Scan page, where scanning lives.
26
+ - The window title is **Six Degrees** (it was "6 Degrees of Separation — LinkedIn Network
27
+ Research"), and the page description no longer claims to be a research project.
28
+ - The import page's "close the tab" note is true in the Mac app too.
29
+ - **Big LinkedIn exports import.** An export over about 9,000 connections was refused ("too large to hold in this browser tab"). The tab now keeps only what the export says about each person and scores it again on load, so exports up to LinkedIn's 30,000 maximum fit. Checked in a browser: 10,000 connections import and draw in about 4 seconds, 30,000 in about 25. It still never touches the database.
30
+ - **`npx six-degrees` downloads about 22 MB instead of about 235 MB.** The package already
31
+ carries its built server; Next, React and d3 are now build-time only.
32
+ - On Windows, npm now refuses the package with a clear "not supported" message instead of
33
+ installing it and crashing at start.
34
+ - On Linux without a desktop, `npx six-degrees` prints the address to open instead of a
35
+ crash when there's no browser to launch.
36
+ - The Scan page checks for Google Chrome on Linux too (where Playwright looks for it).
37
+ - Updating an npx copy says to stop it first, and keeps `--data-dir` if it was started
38
+ with one, so the new version finds your network.
39
+
40
+ ### Changed
41
+ - The app says **scanner** and **scan** everywhere you read it (the Scan page, the side
42
+ panel, the profile page, the import page); the code names are unchanged.
43
+ - Releases publish to npm through npm's trusted publishing: GitHub vouches for the release
44
+ workflow, so no npm token is stored anywhere. Each release builds the package on Linux,
45
+ installs it, checks it serves the app, and publishes that exact file.
46
+ - The README: macOS 15's second button is **Open Anyway**; the LinkedIn warning's link no
47
+ longer shows a file name; the release notes link the changelog.
48
+ - SECURITY.md lists all six gated routes (it said four).
49
+
9
50
  ## [0.2.0] - 2026-09-24
10
51
 
11
52
  ### Changed
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <a href="https://blakeb056.github.io/six-degrees/"><img src="desktop/icon/icon.svg" width="128" height="128" alt="Six Degrees website"></a>
3
+ </p>
4
+
1
5
  <h1 align="center">Six Degrees</h1>
2
6
 
3
7
  <p align="center">
@@ -5,32 +9,16 @@
5
9
  and find the shortest path to someone you haven't met.</em>
6
10
  </p>
7
11
 
8
- <p align="center">
9
- <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Apple-Silicon.dmg"><img src="https://img.shields.io/badge/Download-Apple%20Silicon%20(M1%20or%20newer)-2ea44f?style=for-the-badge&logo=apple&logoColor=white" alt="Download for a Mac with Apple Silicon (M1 or newer)"></a>
10
- &nbsp;
11
- <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg"><img src="https://img.shields.io/badge/Download-Intel%20Mac-555555?style=for-the-badge&logo=apple&logoColor=white" alt="Download for a Mac with an Intel processor"></a>
12
- </p>
12
+ ## Install it
13
13
 
14
- <p align="center"><sub>Free Mac app · macOS 13.5 or later · <a href="#install-it">how to install</a> · <a href="https://github.com/blakeb056/six-degrees/releases/latest">what's new</a></sub></p>
14
+ <p align="center"><strong>Click the button for your Mac to download the app.</strong></p>
15
15
 
16
16
  <p align="center">
17
- <img src="docs/img/app-window.png" alt="Six Degrees on a Mac: your network drawn as circles around you, the most powerful people closest" width="100%">
17
+ <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Apple-Silicon.dmg"><img src="docs/img/download-apple-silicon.png" width="346" alt="Download Six Degrees for a Mac with Apple Silicon (M1 or newer)"></a>
18
+ <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg"><img src="docs/img/download-intel.png" width="346" alt="Download Six Degrees for a Mac with an Intel chip"></a>
18
19
  </p>
19
20
 
20
- <p align="center"><sub>Every person shown in this README is invented — see <a href="scripts/gen-synthetic.mjs"><code>gen-synthetic.mjs</code></a>.</sub></p>
21
-
22
- ---
23
-
24
- Your network has a shape, and you can't see it. Six Degrees draws it: everyone you
25
- know placed on rings by how much they can open up for you, the handful of people
26
- whose own circles reach the furthest, and the shortest chain from you to a stranger
27
- worth meeting.
28
-
29
- It's a Mac app. It runs on your computer, against your own data, with no account and
30
- no server. (Inside the app the logo reads *6 Degrees*; in Applications it's **Six
31
- Degrees**.)
32
-
33
- ## Install it
21
+ <p align="center"><sub>Free Mac app · macOS 13.5 or later · <a href="https://blakeb056.github.io/six-degrees/">website</a> · <a href="https://github.com/blakeb056/six-degrees/releases/latest">what's new</a></sub></p>
34
22
 
35
23
  **Which download?** Apple menu → **About This Mac**. If it says "Chip: Apple M…", take
36
24
  **Apple Silicon**. If it says "Processor: …Intel…", take **Intel**. It needs **macOS 13.5
@@ -45,8 +33,8 @@ paid Apple certificate yet:
45
33
  malware. Close that message with **Done** (on macOS 13 or 14 it may say **OK** or
46
34
  **Cancel**). Anything except *Move to Trash*.
47
35
  3. Open **System Settings → Privacy & Security**, scroll down to *"Six Degrees" was
48
- blocked*, and click **Open Anyway**. Confirm with **Open** (and your password if
49
- asked).
36
+ blocked*, and click **Open Anyway**. Confirm with **Open Anyway** again (on macOS
37
+ 13 or 14 the button says **Open**), and enter your password if asked.
50
38
 
51
39
  That's once per download; after that it opens like any app.
52
40
 
@@ -72,11 +60,26 @@ Your network isn't stored in the app. It's in a hidden folder in your home folde
72
60
  touch it. Each new version also copies your data into its `backups` folder before it
73
61
  first opens it.
74
62
 
75
- **Linux:** run it from source (below); it's tested on Ubuntu.
76
- **Windows:** not yet. A Windows app is planned; nothing is released.
63
+ **Linux:** install **Node 22.13 or later** (from nodejs.org or nvm; Ubuntu's own
64
+ `nodejs` package is too old), then run:
65
+
66
+ ```bash
67
+ npx six-degrees
68
+ ```
69
+
70
+ It opens http://127.0.0.1:6363 in your browser (`--no-open` on a machine without a
71
+ desktop), keeps your data in `~/.six-degrees` (it prints the folder when it starts;
72
+ `--data-dir` puts it elsewhere), and stops with Ctrl-C. To update, stop it and run
73
+ `npx six-degrees@latest`; the Updates panel at the bottom of the Scan page gives the same
74
+ line. Scanning LinkedIn also needs Google Chrome (not Chromium) and Python 3.9+ with venv
75
+ (on Ubuntu: `sudo apt install python3-venv`). It's tested on Ubuntu, and works on a Mac
76
+ too if you'd rather not install the app.
77
+
78
+ **Windows:** not yet. There's no Windows app, and `npx six-degrees` doesn't run on
79
+ Windows yet; npm says so if you try.
77
80
 
78
81
  <details>
79
- <summary>Run it from source (Linux, or a Mac without the app)</summary>
82
+ <summary>Run it from source (contributors)</summary>
80
83
 
81
84
  Needs **Node 22.13 or later** and git.
82
85
 
@@ -93,6 +96,23 @@ To update later: stop it (Ctrl-C), then
93
96
  Scanning LinkedIn also needs Python 3.9+ and Google Chrome (see below).
94
97
  </details>
95
98
 
99
+ ## What it is
100
+
101
+ <p align="center">
102
+ <img src="docs/img/app-window.png" alt="Six Degrees on a Mac: your network drawn as circles around you, the most powerful people closest" width="100%">
103
+ </p>
104
+
105
+ <p align="center"><sub>Every person shown in this README is invented — see <a href="scripts/gen-synthetic.mjs"><code>gen-synthetic.mjs</code></a>.</sub></p>
106
+
107
+ Your network has a shape, and you can't see it. Six Degrees draws it: everyone you
108
+ know placed on rings by how much they can open up for you, the handful of people
109
+ whose own circles reach the furthest, and the shortest chain from you to a stranger
110
+ worth meeting.
111
+
112
+ It's a Mac app (or `npx six-degrees` on Linux). It runs on your computer, against your own data, with no account and
113
+ no server. (Inside the app the logo reads *6 Degrees*; in Applications it's **Six
114
+ Degrees**.)
115
+
96
116
  ## Before you scan LinkedIn
97
117
 
98
118
  > [!WARNING]
@@ -108,7 +128,7 @@ Scanning LinkedIn also needs Python 3.9+ and Google Chrome (see below).
108
128
  > stops at the first sign of push-back. The risk is still yours.
109
129
  >
110
130
  > **The CSV import and the sample network carry no LinkedIn risk at all.** Details:
111
- > [`docs/SCRAPING.md`](docs/SCRAPING.md).
131
+ > [how scanning works and what it risks](docs/SCRAPING.md).
112
132
 
113
133
  ## First run
114
134
 
@@ -116,7 +136,7 @@ The app opens on a welcome screen with three ways in, and asks nothing about you
116
136
 
117
137
  - **Scan my LinkedIn**: a guided page that ticks each step off as it goes. It sets up
118
138
  the scanner in one click, you sign into LinkedIn yourself in a Chrome window, and then
119
- it scans. Needs Google Chrome (in Applications) and Python 3.9+; the page checks that
139
+ it scans. Needs Google Chrome and Python 3.9+; the page checks that
120
140
  both are installed and says what's missing. The app marks this *Recommended* because
121
141
  it's the only way to Degrees and Outlink. Read the warning above first.
122
142
  - **Import my LinkedIn CSV**: LinkedIn's official export, read on your machine. On
@@ -251,7 +271,7 @@ Nothing to configure. Two optional environment variables exist:
251
271
 
252
272
  | Variable | Purpose |
253
273
  |---|---|
254
- | `SIX_DEGREES_HOME` | Where your data lives. Defaults to `~/.six-degrees`. It's read at launch, so it applies when you run from source, not when the Mac app is opened from the Dock or Finder. |
274
+ | `SIX_DEGREES_HOME` | Where your data lives. Defaults to `~/.six-degrees`. It's read at launch, so it applies to `npx six-degrees` and source runs (npx also takes `--data-dir`; see `npx six-degrees --help`), not when the Mac app is opened from the Dock or Finder. |
255
275
  | `ADMIN_TOKEN` | Not needed on your own computer. The app listens only on 127.0.0.1 and isn't built to be exposed: don't put it behind a tunnel or bind it to another address ([SECURITY.md](SECURITY.md)). If it's ever bound elsewhere, the six routes that delete data, run the scanner or update a copy run from source refuse every caller without this token, including the app's own buttons. Everything else, including reading your whole network, stays open. |
256
276
 
257
277
  ## Contributing
package/SECURITY.md CHANGED
@@ -12,17 +12,20 @@ This app is designed to run **on your own machine, against your own network**.
12
12
 
13
13
  - The web UI binds to localhost. It is not hardened for exposure to the
14
14
  internet, and it has no multi-user authentication or per-user authorization.
15
- - Four API routes (`admin-delete`, `admin-update`, `delete-cluster`,
16
- `setup-profile`) can irreversibly destroy or rewrite data. On localhost they
17
- are allowed without a token — anyone who can reach 127.0.0.1 can already open
18
- the database file directly, so a token there adds friction rather than
19
- safety. Reached from any other host they require `ADMIN_TOKEN` as a bearer,
20
- and refuse to run when it is unset.
15
+ - Six API routes are gated (`admin-delete`, `admin-update`, `delete-cluster`,
16
+ `setup-profile`, `scraper`, `update`). The first four can irreversibly
17
+ destroy or rewrite data, `scraper` starts processes, and `update` changes the
18
+ code that runs next. While the server is bound to 127.0.0.1 they need no
19
+ token — anyone who can reach it can already open the database file directly,
20
+ so a token there adds friction rather than safety. If it is bound to any
21
+ other address, every caller must send `ADMIN_TOKEN` as a bearer, including
22
+ this machine and the app's own buttons, and the routes refuse to run when it
23
+ is unset.
21
24
  - A localhost-bound service is still reachable by any web page you visit — the
22
25
  binding keeps other machines out, not your own browser. Every mutating API
23
26
  request is therefore refused when the browser reports it came from another
24
27
  site (`Sec-Fetch-Site`, with `Origin` as a fallback). Command-line callers
25
- such as the scraper send neither header and are unaffected.
28
+ such as the scanner send neither header and are unaffected.
26
29
  - Values stored in the database are treated as untrusted text and escaped
27
30
  before rendering. Do not reintroduce `innerHTML` (including d3's `.html()`)
28
31
  for anything data-bearing.
@@ -34,10 +37,10 @@ Everything stays local:
34
37
 
35
38
  - A CSV import is parsed in the browser and held for that tab only. It is never
36
39
  uploaded and never written to a database.
37
- - Scraped data and cached avatars are written to your machine and are gitignored.
40
+ - Scanned data and cached avatars are written to your machine and are gitignored.
38
41
  - There is no telemetry, no analytics, and no crash reporting.
39
42
 
40
- If you run the scraper, `chrome-profile/` in the data directory holds a live
43
+ If you run the scanner, `chrome-profile/` in the data directory holds a live
41
44
  logged-in LinkedIn session. Never copy, sync, or commit it.
42
45
 
43
46
  Never commit `public/avatars/` or any exported CSV — they contain real people.
@@ -84,10 +84,15 @@ function freePort(start, attempts = 20) {
84
84
  function openBrowser(url) {
85
85
  const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
86
86
  const args = platform() === 'win32' ? ['', url] : [url];
87
+ // A machine with no desktop (no xdg-open) can't open one; say where to go.
88
+ // spawn reports a missing command as an 'error' event, not a throw.
89
+ const instead = () => console.log(` Open ${url} in your browser.`);
87
90
  try {
88
- spawn(cmd, args, { stdio: 'ignore', detached: true, shell: platform() === 'win32' }).unref();
91
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true, shell: platform() === 'win32' });
92
+ child.on('error', instead);
93
+ child.unref();
89
94
  } catch {
90
- /* a browser that will not open is not worth failing over */
95
+ instead();
91
96
  }
92
97
  }
93
98
 
package/lib/csv.js CHANGED
@@ -56,6 +56,34 @@ function hashId(input) {
56
56
  return `csv-${Math.abs(h).toString(36)}`;
57
57
  }
58
58
 
59
+ /** One connection, built from what the export itself says about the person. */
60
+ function connectionFrom({ id, name, role, company, profileUrl, connected_date }) {
61
+ // /paths and the sidebar read seniority off `headline`, not `role`, so
62
+ // rebuild the "Role at Company" shape the scanner would have produced.
63
+ const headline = role && company ? `${role} at ${company}` : role || company || name;
64
+ return {
65
+ id,
66
+ degree: 1,
67
+ name,
68
+ headline,
69
+ role,
70
+ company,
71
+ profile_url: profileUrl || null,
72
+ profile_image_url: null, // the official export carries no photos
73
+ connected_date,
74
+ source_connection_id: null,
75
+ is_catalyst: false,
76
+ catalyst_score: null,
77
+ circle_power: null,
78
+ circle_s_count: null,
79
+ circle_a_count: null,
80
+ circle_elite_pct: null,
81
+ outreach_status: null,
82
+ unlock_status: null,
83
+ ...scoreRecord(headline, company),
84
+ };
85
+ }
86
+
59
87
  // ── The importer ───────────────────────────────────────────────────────────
60
88
  export function parseConnectionsCsv(text) {
61
89
  const rows = parseCsv(text);
@@ -90,10 +118,6 @@ export function parseConnectionsCsv(text) {
90
118
  const role = cell(iPosition);
91
119
  const profileUrl = cell(iUrl);
92
120
 
93
- // /paths and the sidebar read seniority off `headline`, not `role`, so
94
- // rebuild the "Role at Company" shape the scraper would have produced.
95
- const headline = role && company ? `${role} at ${company}` : role || company || name;
96
-
97
121
  const id = profileUrl ? hashId(profileUrl) : `csv-row-${connections.length}`;
98
122
  if (seen.has(id)) { skipped++; continue; }
99
123
  seen.add(id);
@@ -105,27 +129,7 @@ export function parseConnectionsCsv(text) {
105
129
  if (!isNaN(d.getTime())) connected_date = d.toISOString().split('T')[0];
106
130
  }
107
131
 
108
- connections.push({
109
- id,
110
- degree: 1,
111
- name,
112
- headline,
113
- role,
114
- company,
115
- profile_url: profileUrl || null,
116
- profile_image_url: null, // the official export carries no photos
117
- connected_date,
118
- source_connection_id: null,
119
- is_catalyst: false,
120
- catalyst_score: null,
121
- circle_power: null,
122
- circle_s_count: null,
123
- circle_a_count: null,
124
- circle_elite_pct: null,
125
- outreach_status: null,
126
- unlock_status: null,
127
- ...scoreRecord(headline, company),
128
- });
132
+ connections.push(connectionFrom({ id, name, role, company, profileUrl, connected_date }));
129
133
  }
130
134
 
131
135
  if (!connections.length) {
@@ -148,8 +152,29 @@ export function saveSessionNetwork({ degree1 = [], degree2 = [], source = 'csv'
148
152
  }
149
153
  }
150
154
 
155
+ // A scored row is ~540 characters and the tab holds ~5 million, which capped an
156
+ // import near 9,000 connections. So the tab keeps only what the export says about
157
+ // each person (~130 characters) and scores it again on load: an export at
158
+ // LinkedIn's own maximum of 30,000 connections fits.
159
+ export function packConnections(connections) {
160
+ return connections.map((c) => [c.id, c.name, c.role, c.company, c.profile_url, c.connected_date]);
161
+ }
162
+
163
+ export function unpackConnections(rows = []) {
164
+ return rows
165
+ .map(([id, name, role, company, profileUrl, connected_date]) => connectionFrom({ id, name, role, company, profileUrl, connected_date }))
166
+ .sort((a, b) => b.power_score - a.power_score);
167
+ }
168
+
151
169
  export function saveCsvNetwork(connections) {
152
- return saveSessionNetwork({ degree1: connections, degree2: [], source: 'csv' });
170
+ try {
171
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
172
+ packed: packConnections(connections), degree2: [], source: 'csv', importedAt: Date.now(),
173
+ }));
174
+ return true;
175
+ } catch {
176
+ return false; // quota
177
+ }
153
178
  }
154
179
 
155
180
  export function hasCsvNetwork() {
@@ -161,7 +186,8 @@ export function loadCsvNetwork() {
161
186
  const raw = sessionStorage.getItem(STORAGE_KEY);
162
187
  if (!raw) return null;
163
188
  const parsed = JSON.parse(raw);
164
- return { degree1: parsed.degree1 || [], degree2: parsed.degree2 || [], source: parsed.source || 'csv' };
189
+ const degree1 = parsed.packed ? unpackConnections(parsed.packed) : parsed.degree1 || [];
190
+ return { degree1, degree2: parsed.degree2 || [], source: parsed.source || 'csv' };
165
191
  } catch { return null; }
166
192
  }
167
193
 
package/lib/release.js CHANGED
@@ -39,12 +39,15 @@ export function installKind(env = process.env, root = '') {
39
39
  }
40
40
 
41
41
  /** What to run to update, for each way of installing. */
42
- export function updateCommand(kind, slug) {
42
+ export function updateCommand(kind, slug, { dataDir } = {}) {
43
43
  if (kind === 'mac-app') {
44
44
  return `curl -fsSL https://raw.githubusercontent.com/${slug}/main/install.sh | bash`;
45
45
  }
46
46
  if (kind === 'source') {
47
47
  return 'git pull && npm ci && npm run build && npm run start:packaged';
48
48
  }
49
- return 'npx six-degrees@latest';
49
+ // A copy started with --data-dir must be restarted with it, or the new
50
+ // version opens an empty network in the default folder.
51
+ const quoted = dataDir && `'${String(dataDir).replace(/'/g, `'\\''`)}'`;
52
+ return quoted ? `npx six-degrees@latest --data-dir ${quoted}` : 'npx six-degrees@latest';
50
53
  }
@@ -20,7 +20,7 @@ export async function startScrape(action, name, extra = {}) {
20
20
  body: JSON.stringify({ action, ...(name ? { name } : {}), ...extra }),
21
21
  });
22
22
  const d = await r.json().catch(() => ({}));
23
- if (!r.ok) throw new Error(d.error || 'Could not start the scraper.');
23
+ if (!r.ok) throw new Error(d.error || 'Could not start the scanner.');
24
24
  return d;
25
25
  }
26
26
 
@@ -48,9 +48,9 @@ export function runScrape(action, { name, onLog, ...extra } = {}) {
48
48
  /** What to tell someone when the scraper is not usable yet. */
49
49
  export function notReadyMessage(status) {
50
50
  if (!status) return 'Could not reach the app.';
51
- if (!status.checks?.scriptsFound) return 'The scraper files are missing from this install.';
51
+ if (!status.checks?.scriptsFound) return 'The scanner files are missing from this install.';
52
52
  if (!status.checks?.python) return 'Python 3 is not installed on this machine.';
53
- if (!status.checks?.dependencies) return 'The scraper is not installed yet — open Scan to set it up.';
53
+ if (!status.checks?.dependencies) return 'The scanner is not installed yet — open Scan to set it up.';
54
54
  if (!status.checks?.chrome) return 'Google Chrome is not installed.';
55
55
  return null;
56
56
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "six-degrees",
3
- "version": "0.2.0",
4
- "description": "Map your professional network as a galaxy \u2014 leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
3
+ "version": "0.2.1",
4
+ "description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
5
5
  "license": "MIT",
6
6
  "author": "Blake Burford",
7
7
  "repository": {
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/blakeb056/six-degrees/issues"
13
13
  },
14
- "homepage": "https://github.com/blakeb056/six-degrees#readme",
14
+ "homepage": "https://blakeb056.github.io/six-degrees/",
15
15
  "keywords": [
16
16
  "linkedin",
17
17
  "network",
@@ -25,6 +25,10 @@
25
25
  "engines": {
26
26
  "node": ">=22.13"
27
27
  },
28
+ "os": [
29
+ "darwin",
30
+ "linux"
31
+ ],
28
32
  "private": false,
29
33
  "bin": {
30
34
  "six-degrees": "bin/six-degrees.mjs"
@@ -73,16 +77,14 @@
73
77
  "desktop": "electron desktop",
74
78
  "update": "node scripts/update.mjs"
75
79
  },
76
- "dependencies": {
77
- "d3": "^7.9.0",
78
- "next": "16.3.6",
79
- "react": "19.2.4",
80
- "react-dom": "19.2.4"
81
- },
82
80
  "devDependencies": {
83
81
  "@electron/packager": "20.3.0",
82
+ "d3": "^7.9.0",
84
83
  "electron": "44.4.5",
85
84
  "eslint": "^9",
86
- "eslint-config-next": "16.3.6"
85
+ "eslint-config-next": "16.3.6",
86
+ "next": "16.3.6",
87
+ "react": "19.2.4",
88
+ "react-dom": "19.2.4"
87
89
  }
88
90
  }