whose-turn 0.0.0-stage → 1.0.0-alpha.3

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 (55) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +244 -2
  4. package/config.example.json +144 -0
  5. package/connectors/demo.mjs +146 -0
  6. package/connectors/github.mjs +316 -0
  7. package/connectors/index.mjs +38 -0
  8. package/connectors/jira.mjs +155 -0
  9. package/connectors/linear.mjs +113 -0
  10. package/dist/assets/_plugin-vue_export-helper-BKveAJb2.css +1 -0
  11. package/dist/assets/_plugin-vue_export-helper-DHr5RHTx.js +18 -0
  12. package/dist/assets/bem-COKTjsky.js +1 -0
  13. package/dist/assets/index-CuyWocyb.js +3 -0
  14. package/dist/assets/index-p8D7awn0.css +1 -0
  15. package/dist/assets/report-BtGnm9ax.js +1 -0
  16. package/dist/assets/report-xVOo7qip.css +1 -0
  17. package/dist/assets/scheme-DJ3WNw60.js +1 -0
  18. package/dist/assets/sheet-CBYGM-FG.js +1 -0
  19. package/dist/assets/sheet-DYpaJaXO.css +1 -0
  20. package/dist/index.html +19 -0
  21. package/dist/report.html +16 -0
  22. package/dist/sheet.html +16 -0
  23. package/dist/vendor/popper.min.js +6 -0
  24. package/dist/vendor/tippy-bundle.umd.min.js +2 -0
  25. package/install.sh +69 -0
  26. package/lib/api.mjs +406 -0
  27. package/lib/backoff.mjs +2 -0
  28. package/lib/compose.mjs +67 -0
  29. package/lib/config.mjs +290 -0
  30. package/lib/default-table.mjs +58 -0
  31. package/lib/errors.mjs +55 -0
  32. package/lib/facts.mjs +128 -0
  33. package/lib/feedback.mjs +148 -0
  34. package/lib/handoff.mjs +127 -0
  35. package/lib/health.mjs +35 -0
  36. package/lib/http-client.mjs +44 -0
  37. package/lib/inbox.mjs +60 -0
  38. package/lib/linear-api.mjs +12 -0
  39. package/lib/notify.mjs +36 -0
  40. package/lib/palette.mjs +213 -0
  41. package/lib/platform/credential.ps1 +84 -0
  42. package/lib/registry.mjs +61 -0
  43. package/lib/rules.mjs +241 -0
  44. package/lib/secrets.mjs +69 -0
  45. package/lib/static.mjs +36 -0
  46. package/lib/store.mjs +61 -0
  47. package/lib/table.mjs +148 -0
  48. package/lib/text.mjs +46 -0
  49. package/lib/token-cache.mjs +12 -0
  50. package/lib/update.mjs +134 -0
  51. package/lib/window.mjs +33 -0
  52. package/lib/workspace.mjs +294 -0
  53. package/open.sh +40 -0
  54. package/package.json +67 -4
  55. package/server.mjs +96 -0
@@ -0,0 +1,2 @@
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t=t||self).tippy=e(t.Popper)}(this,(function(t){"use strict";var e="undefined"!=typeof window&&"undefined"!=typeof document,n=!!e&&!!window.msCrypto,r={passive:!0,capture:!0},o=function(){return document.body};function i(t,e,n){if(Array.isArray(t)){var r=t[e];return null==r?Array.isArray(n)?n[e]:n:r}return t}function a(t,e){var n={}.toString.call(t);return 0===n.indexOf("[object")&&n.indexOf(e+"]")>-1}function s(t,e){return"function"==typeof t?t.apply(void 0,e):t}function u(t,e){return 0===e?t:function(r){clearTimeout(n),n=setTimeout((function(){t(r)}),e)};var n}function p(t,e){var n=Object.assign({},t);return e.forEach((function(t){delete n[t]})),n}function c(t){return[].concat(t)}function f(t,e){-1===t.indexOf(e)&&t.push(e)}function l(t){return t.split("-")[0]}function d(t){return[].slice.call(t)}function v(t){return Object.keys(t).reduce((function(e,n){return void 0!==t[n]&&(e[n]=t[n]),e}),{})}function m(){return document.createElement("div")}function g(t){return["Element","Fragment"].some((function(e){return a(t,e)}))}function h(t){return a(t,"MouseEvent")}function b(t){return!(!t||!t._tippy||t._tippy.reference!==t)}function y(t){return g(t)?[t]:function(t){return a(t,"NodeList")}(t)?d(t):Array.isArray(t)?t:d(document.querySelectorAll(t))}function w(t,e){t.forEach((function(t){t&&(t.style.transitionDuration=e+"ms")}))}function x(t,e){t.forEach((function(t){t&&t.setAttribute("data-state",e)}))}function E(t){var e,n=c(t)[0];return null!=n&&null!=(e=n.ownerDocument)&&e.body?n.ownerDocument:document}function O(t,e,n){var r=e+"EventListener";["transitionend","webkitTransitionEnd"].forEach((function(e){t[r](e,n)}))}function C(t,e){for(var n=e;n;){var r;if(t.contains(n))return!0;n=null==n.getRootNode||null==(r=n.getRootNode())?void 0:r.host}return!1}var T={isTouch:!1},A=0;function L(){T.isTouch||(T.isTouch=!0,window.performance&&document.addEventListener("mousemove",D))}function D(){var t=performance.now();t-A<20&&(T.isTouch=!1,document.removeEventListener("mousemove",D)),A=t}function k(){var t=document.activeElement;if(b(t)){var e=t._tippy;t.blur&&!e.state.isVisible&&t.blur()}}var R=Object.assign({appendTo:o,aria:{content:"auto",expanded:"auto"},delay:0,duration:[300,250],getReferenceClientRect:null,hideOnClick:!0,ignoreAttributes:!1,interactive:!1,interactiveBorder:2,interactiveDebounce:0,moveTransition:"",offset:[0,10],onAfterUpdate:function(){},onBeforeUpdate:function(){},onCreate:function(){},onDestroy:function(){},onHidden:function(){},onHide:function(){},onMount:function(){},onShow:function(){},onShown:function(){},onTrigger:function(){},onUntrigger:function(){},onClickOutside:function(){},placement:"top",plugins:[],popperOptions:{},render:null,showOnCreate:!1,touch:!0,trigger:"mouseenter focus",triggerTarget:null},{animateFill:!1,followCursor:!1,inlinePositioning:!1,sticky:!1},{allowHTML:!1,animation:"fade",arrow:!0,content:"",inertia:!1,maxWidth:350,role:"tooltip",theme:"",zIndex:9999}),P=Object.keys(R);function j(t){var e=(t.plugins||[]).reduce((function(e,n){var r,o=n.name,i=n.defaultValue;o&&(e[o]=void 0!==t[o]?t[o]:null!=(r=R[o])?r:i);return e}),{});return Object.assign({},t,e)}function M(t,e){var n=Object.assign({},e,{content:s(e.content,[t])},e.ignoreAttributes?{}:function(t,e){return(e?Object.keys(j(Object.assign({},R,{plugins:e}))):P).reduce((function(e,n){var r=(t.getAttribute("data-tippy-"+n)||"").trim();if(!r)return e;if("content"===n)e[n]=r;else try{e[n]=JSON.parse(r)}catch(t){e[n]=r}return e}),{})}(t,e.plugins));return n.aria=Object.assign({},R.aria,n.aria),n.aria={expanded:"auto"===n.aria.expanded?e.interactive:n.aria.expanded,content:"auto"===n.aria.content?e.interactive?null:"describedby":n.aria.content},n}function V(t,e){t.innerHTML=e}function I(t){var e=m();return!0===t?e.className="tippy-arrow":(e.className="tippy-svg-arrow",g(t)?e.appendChild(t):V(e,t)),e}function S(t,e){g(e.content)?(V(t,""),t.appendChild(e.content)):"function"!=typeof e.content&&(e.allowHTML?V(t,e.content):t.textContent=e.content)}function B(t){var e=t.firstElementChild,n=d(e.children);return{box:e,content:n.find((function(t){return t.classList.contains("tippy-content")})),arrow:n.find((function(t){return t.classList.contains("tippy-arrow")||t.classList.contains("tippy-svg-arrow")})),backdrop:n.find((function(t){return t.classList.contains("tippy-backdrop")}))}}function N(t){var e=m(),n=m();n.className="tippy-box",n.setAttribute("data-state","hidden"),n.setAttribute("tabindex","-1");var r=m();function o(n,r){var o=B(e),i=o.box,a=o.content,s=o.arrow;r.theme?i.setAttribute("data-theme",r.theme):i.removeAttribute("data-theme"),"string"==typeof r.animation?i.setAttribute("data-animation",r.animation):i.removeAttribute("data-animation"),r.inertia?i.setAttribute("data-inertia",""):i.removeAttribute("data-inertia"),i.style.maxWidth="number"==typeof r.maxWidth?r.maxWidth+"px":r.maxWidth,r.role?i.setAttribute("role",r.role):i.removeAttribute("role"),n.content===r.content&&n.allowHTML===r.allowHTML||S(a,t.props),r.arrow?s?n.arrow!==r.arrow&&(i.removeChild(s),i.appendChild(I(r.arrow))):i.appendChild(I(r.arrow)):s&&i.removeChild(s)}return r.className="tippy-content",r.setAttribute("data-state","hidden"),S(r,t.props),e.appendChild(n),n.appendChild(r),o(t.props,t.props),{popper:e,onUpdate:o}}N.$$tippy=!0;var H=1,U=[],_=[];function z(e,a){var p,g,b,y,A,L,D,k,P=M(e,Object.assign({},R,j(v(a)))),V=!1,I=!1,S=!1,N=!1,z=[],F=u(wt,P.interactiveDebounce),W=H++,X=(k=P.plugins).filter((function(t,e){return k.indexOf(t)===e})),Y={id:W,reference:e,popper:m(),popperInstance:null,props:P,state:{isEnabled:!0,isVisible:!1,isDestroyed:!1,isMounted:!1,isShown:!1},plugins:X,clearDelayTimeouts:function(){clearTimeout(p),clearTimeout(g),cancelAnimationFrame(b)},setProps:function(t){if(Y.state.isDestroyed)return;at("onBeforeUpdate",[Y,t]),bt();var n=Y.props,r=M(e,Object.assign({},n,v(t),{ignoreAttributes:!0}));Y.props=r,ht(),n.interactiveDebounce!==r.interactiveDebounce&&(pt(),F=u(wt,r.interactiveDebounce));n.triggerTarget&&!r.triggerTarget?c(n.triggerTarget).forEach((function(t){t.removeAttribute("aria-expanded")})):r.triggerTarget&&e.removeAttribute("aria-expanded");ut(),it(),J&&J(n,r);Y.popperInstance&&(Ct(),At().forEach((function(t){requestAnimationFrame(t._tippy.popperInstance.forceUpdate)})));at("onAfterUpdate",[Y,t])},setContent:function(t){Y.setProps({content:t})},show:function(){var t=Y.state.isVisible,e=Y.state.isDestroyed,n=!Y.state.isEnabled,r=T.isTouch&&!Y.props.touch,a=i(Y.props.duration,0,R.duration);if(t||e||n||r)return;if(et().hasAttribute("disabled"))return;if(at("onShow",[Y],!1),!1===Y.props.onShow(Y))return;Y.state.isVisible=!0,tt()&&($.style.visibility="visible");it(),dt(),Y.state.isMounted||($.style.transition="none");if(tt()){var u=rt(),p=u.box,c=u.content;w([p,c],0)}L=function(){var t;if(Y.state.isVisible&&!N){if(N=!0,$.offsetHeight,$.style.transition=Y.props.moveTransition,tt()&&Y.props.animation){var e=rt(),n=e.box,r=e.content;w([n,r],a),x([n,r],"visible")}st(),ut(),f(_,Y),null==(t=Y.popperInstance)||t.forceUpdate(),at("onMount",[Y]),Y.props.animation&&tt()&&function(t,e){mt(t,e)}(a,(function(){Y.state.isShown=!0,at("onShown",[Y])}))}},function(){var t,e=Y.props.appendTo,n=et();t=Y.props.interactive&&e===o||"parent"===e?n.parentNode:s(e,[n]);t.contains($)||t.appendChild($);Y.state.isMounted=!0,Ct()}()},hide:function(){var t=!Y.state.isVisible,e=Y.state.isDestroyed,n=!Y.state.isEnabled,r=i(Y.props.duration,1,R.duration);if(t||e||n)return;if(at("onHide",[Y],!1),!1===Y.props.onHide(Y))return;Y.state.isVisible=!1,Y.state.isShown=!1,N=!1,V=!1,tt()&&($.style.visibility="hidden");if(pt(),vt(),it(!0),tt()){var o=rt(),a=o.box,s=o.content;Y.props.animation&&(w([a,s],r),x([a,s],"hidden"))}st(),ut(),Y.props.animation?tt()&&function(t,e){mt(t,(function(){!Y.state.isVisible&&$.parentNode&&$.parentNode.contains($)&&e()}))}(r,Y.unmount):Y.unmount()},hideWithInteractivity:function(t){nt().addEventListener("mousemove",F),f(U,F),F(t)},enable:function(){Y.state.isEnabled=!0},disable:function(){Y.hide(),Y.state.isEnabled=!1},unmount:function(){Y.state.isVisible&&Y.hide();if(!Y.state.isMounted)return;Tt(),At().forEach((function(t){t._tippy.unmount()})),$.parentNode&&$.parentNode.removeChild($);_=_.filter((function(t){return t!==Y})),Y.state.isMounted=!1,at("onHidden",[Y])},destroy:function(){if(Y.state.isDestroyed)return;Y.clearDelayTimeouts(),Y.unmount(),bt(),delete e._tippy,Y.state.isDestroyed=!0,at("onDestroy",[Y])}};if(!P.render)return Y;var q=P.render(Y),$=q.popper,J=q.onUpdate;$.setAttribute("data-tippy-root",""),$.id="tippy-"+Y.id,Y.popper=$,e._tippy=Y,$._tippy=Y;var G=X.map((function(t){return t.fn(Y)})),K=e.hasAttribute("aria-expanded");return ht(),ut(),it(),at("onCreate",[Y]),P.showOnCreate&&Lt(),$.addEventListener("mouseenter",(function(){Y.props.interactive&&Y.state.isVisible&&Y.clearDelayTimeouts()})),$.addEventListener("mouseleave",(function(){Y.props.interactive&&Y.props.trigger.indexOf("mouseenter")>=0&&nt().addEventListener("mousemove",F)})),Y;function Q(){var t=Y.props.touch;return Array.isArray(t)?t:[t,0]}function Z(){return"hold"===Q()[0]}function tt(){var t;return!(null==(t=Y.props.render)||!t.$$tippy)}function et(){return D||e}function nt(){var t=et().parentNode;return t?E(t):document}function rt(){return B($)}function ot(t){return Y.state.isMounted&&!Y.state.isVisible||T.isTouch||y&&"focus"===y.type?0:i(Y.props.delay,t?0:1,R.delay)}function it(t){void 0===t&&(t=!1),$.style.pointerEvents=Y.props.interactive&&!t?"":"none",$.style.zIndex=""+Y.props.zIndex}function at(t,e,n){var r;(void 0===n&&(n=!0),G.forEach((function(n){n[t]&&n[t].apply(n,e)})),n)&&(r=Y.props)[t].apply(r,e)}function st(){var t=Y.props.aria;if(t.content){var n="aria-"+t.content,r=$.id;c(Y.props.triggerTarget||e).forEach((function(t){var e=t.getAttribute(n);if(Y.state.isVisible)t.setAttribute(n,e?e+" "+r:r);else{var o=e&&e.replace(r,"").trim();o?t.setAttribute(n,o):t.removeAttribute(n)}}))}}function ut(){!K&&Y.props.aria.expanded&&c(Y.props.triggerTarget||e).forEach((function(t){Y.props.interactive?t.setAttribute("aria-expanded",Y.state.isVisible&&t===et()?"true":"false"):t.removeAttribute("aria-expanded")}))}function pt(){nt().removeEventListener("mousemove",F),U=U.filter((function(t){return t!==F}))}function ct(t){if(!T.isTouch||!S&&"mousedown"!==t.type){var n=t.composedPath&&t.composedPath()[0]||t.target;if(!Y.props.interactive||!C($,n)){if(c(Y.props.triggerTarget||e).some((function(t){return C(t,n)}))){if(T.isTouch)return;if(Y.state.isVisible&&Y.props.trigger.indexOf("click")>=0)return}else at("onClickOutside",[Y,t]);!0===Y.props.hideOnClick&&(Y.clearDelayTimeouts(),Y.hide(),I=!0,setTimeout((function(){I=!1})),Y.state.isMounted||vt())}}}function ft(){S=!0}function lt(){S=!1}function dt(){var t=nt();t.addEventListener("mousedown",ct,!0),t.addEventListener("touchend",ct,r),t.addEventListener("touchstart",lt,r),t.addEventListener("touchmove",ft,r)}function vt(){var t=nt();t.removeEventListener("mousedown",ct,!0),t.removeEventListener("touchend",ct,r),t.removeEventListener("touchstart",lt,r),t.removeEventListener("touchmove",ft,r)}function mt(t,e){var n=rt().box;function r(t){t.target===n&&(O(n,"remove",r),e())}if(0===t)return e();O(n,"remove",A),O(n,"add",r),A=r}function gt(t,n,r){void 0===r&&(r=!1),c(Y.props.triggerTarget||e).forEach((function(e){e.addEventListener(t,n,r),z.push({node:e,eventType:t,handler:n,options:r})}))}function ht(){var t;Z()&&(gt("touchstart",yt,{passive:!0}),gt("touchend",xt,{passive:!0})),(t=Y.props.trigger,t.split(/\s+/).filter(Boolean)).forEach((function(t){if("manual"!==t)switch(gt(t,yt),t){case"mouseenter":gt("mouseleave",xt);break;case"focus":gt(n?"focusout":"blur",Et);break;case"focusin":gt("focusout",Et)}}))}function bt(){z.forEach((function(t){var e=t.node,n=t.eventType,r=t.handler,o=t.options;e.removeEventListener(n,r,o)})),z=[]}function yt(t){var e,n=!1;if(Y.state.isEnabled&&!Ot(t)&&!I){var r="focus"===(null==(e=y)?void 0:e.type);y=t,D=t.currentTarget,ut(),!Y.state.isVisible&&h(t)&&U.forEach((function(e){return e(t)})),"click"===t.type&&(Y.props.trigger.indexOf("mouseenter")<0||V)&&!1!==Y.props.hideOnClick&&Y.state.isVisible?n=!0:Lt(t),"click"===t.type&&(V=!n),n&&!r&&Dt(t)}}function wt(t){var e=t.target,n=et().contains(e)||$.contains(e);"mousemove"===t.type&&n||function(t,e){var n=e.clientX,r=e.clientY;return t.every((function(t){var e=t.popperRect,o=t.popperState,i=t.props.interactiveBorder,a=l(o.placement),s=o.modifiersData.offset;if(!s)return!0;var u="bottom"===a?s.top.y:0,p="top"===a?s.bottom.y:0,c="right"===a?s.left.x:0,f="left"===a?s.right.x:0,d=e.top-r+u>i,v=r-e.bottom-p>i,m=e.left-n+c>i,g=n-e.right-f>i;return d||v||m||g}))}(At().concat($).map((function(t){var e,n=null==(e=t._tippy.popperInstance)?void 0:e.state;return n?{popperRect:t.getBoundingClientRect(),popperState:n,props:P}:null})).filter(Boolean),t)&&(pt(),Dt(t))}function xt(t){Ot(t)||Y.props.trigger.indexOf("click")>=0&&V||(Y.props.interactive?Y.hideWithInteractivity(t):Dt(t))}function Et(t){Y.props.trigger.indexOf("focusin")<0&&t.target!==et()||Y.props.interactive&&t.relatedTarget&&$.contains(t.relatedTarget)||Dt(t)}function Ot(t){return!!T.isTouch&&Z()!==t.type.indexOf("touch")>=0}function Ct(){Tt();var n=Y.props,r=n.popperOptions,o=n.placement,i=n.offset,a=n.getReferenceClientRect,s=n.moveTransition,u=tt()?B($).arrow:null,p=a?{getBoundingClientRect:a,contextElement:a.contextElement||et()}:e,c=[{name:"offset",options:{offset:i}},{name:"preventOverflow",options:{padding:{top:2,bottom:2,left:5,right:5}}},{name:"flip",options:{padding:5}},{name:"computeStyles",options:{adaptive:!s}},{name:"$$tippy",enabled:!0,phase:"beforeWrite",requires:["computeStyles"],fn:function(t){var e=t.state;if(tt()){var n=rt().box;["placement","reference-hidden","escaped"].forEach((function(t){"placement"===t?n.setAttribute("data-placement",e.placement):e.attributes.popper["data-popper-"+t]?n.setAttribute("data-"+t,""):n.removeAttribute("data-"+t)})),e.attributes.popper={}}}}];tt()&&u&&c.push({name:"arrow",options:{element:u,padding:3}}),c.push.apply(c,(null==r?void 0:r.modifiers)||[]),Y.popperInstance=t.createPopper(p,$,Object.assign({},r,{placement:o,onFirstUpdate:L,modifiers:c}))}function Tt(){Y.popperInstance&&(Y.popperInstance.destroy(),Y.popperInstance=null)}function At(){return d($.querySelectorAll("[data-tippy-root]"))}function Lt(t){Y.clearDelayTimeouts(),t&&at("onTrigger",[Y,t]),dt();var e=ot(!0),n=Q(),r=n[0],o=n[1];T.isTouch&&"hold"===r&&o&&(e=o),e?p=setTimeout((function(){Y.show()}),e):Y.show()}function Dt(t){if(Y.clearDelayTimeouts(),at("onUntrigger",[Y,t]),Y.state.isVisible){if(!(Y.props.trigger.indexOf("mouseenter")>=0&&Y.props.trigger.indexOf("click")>=0&&["mouseleave","mousemove"].indexOf(t.type)>=0&&V)){var e=ot(!1);e?g=setTimeout((function(){Y.state.isVisible&&Y.hide()}),e):b=requestAnimationFrame((function(){Y.hide()}))}}else vt()}}function F(t,e){void 0===e&&(e={});var n=R.plugins.concat(e.plugins||[]);document.addEventListener("touchstart",L,r),window.addEventListener("blur",k);var o=Object.assign({},e,{plugins:n}),i=y(t).reduce((function(t,e){var n=e&&z(e,o);return n&&t.push(n),t}),[]);return g(t)?i[0]:i}F.defaultProps=R,F.setDefaultProps=function(t){Object.keys(t).forEach((function(e){R[e]=t[e]}))},F.currentInput=T;var W=Object.assign({},t.applyStyles,{effect:function(t){var e=t.state,n={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};Object.assign(e.elements.popper.style,n.popper),e.styles=n,e.elements.arrow&&Object.assign(e.elements.arrow.style,n.arrow)}}),X={mouseover:"mouseenter",focusin:"focus",click:"click"};var Y={name:"animateFill",defaultValue:!1,fn:function(t){var e;if(null==(e=t.props.render)||!e.$$tippy)return{};var n=B(t.popper),r=n.box,o=n.content,i=t.props.animateFill?function(){var t=m();return t.className="tippy-backdrop",x([t],"hidden"),t}():null;return{onCreate:function(){i&&(r.insertBefore(i,r.firstElementChild),r.setAttribute("data-animatefill",""),r.style.overflow="hidden",t.setProps({arrow:!1,animation:"shift-away"}))},onMount:function(){if(i){var t=r.style.transitionDuration,e=Number(t.replace("ms",""));o.style.transitionDelay=Math.round(e/10)+"ms",i.style.transitionDuration=t,x([i],"visible")}},onShow:function(){i&&(i.style.transitionDuration="0ms")},onHide:function(){i&&x([i],"hidden")}}}};var q={clientX:0,clientY:0},$=[];function J(t){var e=t.clientX,n=t.clientY;q={clientX:e,clientY:n}}var G={name:"followCursor",defaultValue:!1,fn:function(t){var e=t.reference,n=E(t.props.triggerTarget||e),r=!1,o=!1,i=!0,a=t.props;function s(){return"initial"===t.props.followCursor&&t.state.isVisible}function u(){n.addEventListener("mousemove",f)}function p(){n.removeEventListener("mousemove",f)}function c(){r=!0,t.setProps({getReferenceClientRect:null}),r=!1}function f(n){var r=!n.target||e.contains(n.target),o=t.props.followCursor,i=n.clientX,a=n.clientY,s=e.getBoundingClientRect(),u=i-s.left,p=a-s.top;!r&&t.props.interactive||t.setProps({getReferenceClientRect:function(){var t=e.getBoundingClientRect(),n=i,r=a;"initial"===o&&(n=t.left+u,r=t.top+p);var s="horizontal"===o?t.top:r,c="vertical"===o?t.right:n,f="horizontal"===o?t.bottom:r,l="vertical"===o?t.left:n;return{width:c-l,height:f-s,top:s,right:c,bottom:f,left:l}}})}function l(){t.props.followCursor&&($.push({instance:t,doc:n}),function(t){t.addEventListener("mousemove",J)}(n))}function d(){0===($=$.filter((function(e){return e.instance!==t}))).filter((function(t){return t.doc===n})).length&&function(t){t.removeEventListener("mousemove",J)}(n)}return{onCreate:l,onDestroy:d,onBeforeUpdate:function(){a=t.props},onAfterUpdate:function(e,n){var i=n.followCursor;r||void 0!==i&&a.followCursor!==i&&(d(),i?(l(),!t.state.isMounted||o||s()||u()):(p(),c()))},onMount:function(){t.props.followCursor&&!o&&(i&&(f(q),i=!1),s()||u())},onTrigger:function(t,e){h(e)&&(q={clientX:e.clientX,clientY:e.clientY}),o="focus"===e.type},onHidden:function(){t.props.followCursor&&(c(),p(),i=!0)}}}};var K={name:"inlinePositioning",defaultValue:!1,fn:function(t){var e,n=t.reference;var r=-1,o=!1,i=[],a={name:"tippyInlinePositioning",enabled:!0,phase:"afterWrite",fn:function(o){var a=o.state;t.props.inlinePositioning&&(-1!==i.indexOf(a.placement)&&(i=[]),e!==a.placement&&-1===i.indexOf(a.placement)&&(i.push(a.placement),t.setProps({getReferenceClientRect:function(){return function(t){return function(t,e,n,r){if(n.length<2||null===t)return e;if(2===n.length&&r>=0&&n[0].left>n[1].right)return n[r]||e;switch(t){case"top":case"bottom":var o=n[0],i=n[n.length-1],a="top"===t,s=o.top,u=i.bottom,p=a?o.left:i.left,c=a?o.right:i.right;return{top:s,bottom:u,left:p,right:c,width:c-p,height:u-s};case"left":case"right":var f=Math.min.apply(Math,n.map((function(t){return t.left}))),l=Math.max.apply(Math,n.map((function(t){return t.right}))),d=n.filter((function(e){return"left"===t?e.left===f:e.right===l})),v=d[0].top,m=d[d.length-1].bottom;return{top:v,bottom:m,left:f,right:l,width:l-f,height:m-v};default:return e}}(l(t),n.getBoundingClientRect(),d(n.getClientRects()),r)}(a.placement)}})),e=a.placement)}};function s(){var e;o||(e=function(t,e){var n;return{popperOptions:Object.assign({},t.popperOptions,{modifiers:[].concat(((null==(n=t.popperOptions)?void 0:n.modifiers)||[]).filter((function(t){return t.name!==e.name})),[e])})}}(t.props,a),o=!0,t.setProps(e),o=!1)}return{onCreate:s,onAfterUpdate:s,onTrigger:function(e,n){if(h(n)){var o=d(t.reference.getClientRects()),i=o.find((function(t){return t.left-2<=n.clientX&&t.right+2>=n.clientX&&t.top-2<=n.clientY&&t.bottom+2>=n.clientY})),a=o.indexOf(i);r=a>-1?a:r}},onHidden:function(){r=-1}}}};var Q={name:"sticky",defaultValue:!1,fn:function(t){var e=t.reference,n=t.popper;function r(e){return!0===t.props.sticky||t.props.sticky===e}var o=null,i=null;function a(){var s=r("reference")?(t.popperInstance?t.popperInstance.state.elements.reference:e).getBoundingClientRect():null,u=r("popper")?n.getBoundingClientRect():null;(s&&Z(o,s)||u&&Z(i,u))&&t.popperInstance&&t.popperInstance.update(),o=s,i=u,t.state.isMounted&&requestAnimationFrame(a)}return{onMount:function(){t.props.sticky&&a()}}}};function Z(t,e){return!t||!e||(t.top!==e.top||t.right!==e.right||t.bottom!==e.bottom||t.left!==e.left)}return e&&function(t){var e=document.createElement("style");e.textContent=t,e.setAttribute("data-tippy-stylesheet","");var n=document.head,r=document.querySelector("head>style,head>link");r?n.insertBefore(e,r):n.appendChild(e)}('.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}'),F.setDefaultProps({plugins:[Y,G,K,Q],render:N}),F.createSingleton=function(t,e){var n;void 0===e&&(e={});var r,o=t,i=[],a=[],s=e.overrides,u=[],f=!1;function l(){a=o.map((function(t){return c(t.props.triggerTarget||t.reference)})).reduce((function(t,e){return t.concat(e)}),[])}function d(){i=o.map((function(t){return t.reference}))}function v(t){o.forEach((function(e){t?e.enable():e.disable()}))}function g(t){return o.map((function(e){var n=e.setProps;return e.setProps=function(o){n(o),e.reference===r&&t.setProps(o)},function(){e.setProps=n}}))}function h(t,e){var n=a.indexOf(e);if(e!==r){r=e;var u=(s||[]).concat("content").reduce((function(t,e){return t[e]=o[n].props[e],t}),{});t.setProps(Object.assign({},u,{getReferenceClientRect:"function"==typeof u.getReferenceClientRect?u.getReferenceClientRect:function(){var t;return null==(t=i[n])?void 0:t.getBoundingClientRect()}}))}}v(!1),d(),l();var b={fn:function(){return{onDestroy:function(){v(!0)},onHidden:function(){r=null},onClickOutside:function(t){t.props.showOnCreate&&!f&&(f=!0,r=null)},onShow:function(t){t.props.showOnCreate&&!f&&(f=!0,h(t,i[0]))},onTrigger:function(t,e){h(t,e.currentTarget)}}}},y=F(m(),Object.assign({},p(e,["overrides"]),{plugins:[b].concat(e.plugins||[]),triggerTarget:a,popperOptions:Object.assign({},e.popperOptions,{modifiers:[].concat((null==(n=e.popperOptions)?void 0:n.modifiers)||[],[W])})})),w=y.show;y.show=function(t){if(w(),!r&&null==t)return h(y,i[0]);if(!r||null!=t){if("number"==typeof t)return i[t]&&h(y,i[t]);if(o.indexOf(t)>=0){var e=t.reference;return h(y,e)}return i.indexOf(t)>=0?h(y,t):void 0}},y.showNext=function(){var t=i[0];if(!r)return y.show(0);var e=i.indexOf(r);y.show(i[e+1]||t)},y.showPrevious=function(){var t=i[i.length-1];if(!r)return y.show(t);var e=i.indexOf(r),n=i[e-1]||t;y.show(n)};var x=y.setProps;return y.setProps=function(t){s=t.overrides||s,x(t)},y.setInstances=function(t){v(!0),u.forEach((function(t){return t()})),o=t,v(!1),d(),l(),u=g(y),y.setProps({triggerTarget:a})},u=g(y),y},F.delegate=function(t,e){var n=[],o=[],i=!1,a=e.target,s=p(e,["target"]),u=Object.assign({},s,{trigger:"manual",touch:!1}),f=Object.assign({touch:R.touch},s,{showOnCreate:!0}),l=F(t,u);function d(t){if(t.target&&!i){var n=t.target.closest(a);if(n){var r=n.getAttribute("data-tippy-trigger")||e.trigger||R.trigger;if(!n._tippy&&!("touchstart"===t.type&&"boolean"==typeof f.touch||"touchstart"!==t.type&&r.indexOf(X[t.type])<0)){var s=F(n,f);s&&(o=o.concat(s))}}}}function v(t,e,r,o){void 0===o&&(o=!1),t.addEventListener(e,r,o),n.push({node:t,eventType:e,handler:r,options:o})}return c(l).forEach((function(t){var e=t.destroy,a=t.enable,s=t.disable;t.destroy=function(t){void 0===t&&(t=!0),t&&o.forEach((function(t){t.destroy()})),o=[],n.forEach((function(t){var e=t.node,n=t.eventType,r=t.handler,o=t.options;e.removeEventListener(n,r,o)})),n=[],e()},t.enable=function(){a(),o.forEach((function(t){return t.enable()})),i=!1},t.disable=function(){s(),o.forEach((function(t){return t.disable()})),i=!0},function(t){var e=t.reference;v(e,"touchstart",d,r),v(e,"mouseover",d),v(e,"focusin",d),v(e,"click",d)}(t)})),l},F.hideAll=function(t){var e=void 0===t?{}:t,n=e.exclude,r=e.duration;_.forEach((function(t){var e=!1;if(n&&(e=b(n)?t.reference===n:t.popper===n.popper),!e){var o=t.props.duration;t.setProps({duration:r}),t.hide(),t.state.isDestroyed||t.setProps({duration:o})}}))},F.roundArrow='<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><path d="M0 6s1.796-.013 4.67-3.615C5.851.9 6.93.006 8 0c1.07-.006 2.148.887 3.343 2.385C14.233 6.005 16 6 16 6H0z"></svg>',F}));
2
+ //# sourceMappingURL=tippy-bundle.umd.min.js.map
package/install.sh ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # Installs Whose Turn so it starts at login and restarts if it stops: a launchd agent on macOS, a systemd user unit on Linux.
3
+ # Undo (macOS): launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/local.whose-turn.plist && rm that plist.
4
+ # Undo (Linux): systemctl --user disable --now whose-turn && rm ~/.config/systemd/user/whose-turn.service
5
+ set -e
6
+ DIR=$(cd "$(dirname "$0")" && pwd -P)
7
+ if [[ ! -f $DIR/dist/index.html ]]; then
8
+ echo "No built window in $DIR: dist/index.html is missing. A released whose-turn package carries it; in a development checkout run npm run build first."
9
+ exit 1
10
+ fi
11
+ NODE=$(command -v node) || { echo "Node 22 or newer is needed on PATH."; exit 1; }
12
+ NODE_DIR=$(dirname "$NODE")
13
+ PORT=$("$NODE" --input-type=module -e "const { pathToFileURL } = await import('node:url'); const { defaultDataDir, resolvePort } = await import(pathToFileURL(process.argv[1]).href); console.log(resolvePort(process.env.WHOSE_TURN_DATA || defaultDataDir()))" "$DIR/lib/config.mjs")
14
+ URL="http://127.0.0.1:${PORT:-8765}/"
15
+
16
+ install_macos() {
17
+ local plist=$HOME/Library/LaunchAgents/local.whose-turn.plist
18
+ local log=$HOME/Library/Logs/whose-turn.log
19
+ mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs"
20
+ cat > "$plist" <<PL
21
+ <?xml version="1.0" encoding="UTF-8"?>
22
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
23
+ <plist version="1.0"><dict>
24
+ <key>Label</key><string>local.whose-turn</string>
25
+ <key>ProgramArguments</key><array><string>$NODE</string><string>$DIR/server.mjs</string></array>
26
+ <key>WorkingDirectory</key><string>$DIR</string>
27
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>$NODE_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string></dict>
28
+ <key>RunAtLoad</key><true/>
29
+ <key>KeepAlive</key><true/>
30
+ <key>StandardOutPath</key><string>$log</string>
31
+ <key>StandardErrorPath</key><string>$log</string>
32
+ </dict></plist>
33
+ PL
34
+ launchctl bootout "gui/$(id -u)" "$plist" 2>/dev/null || true
35
+ launchctl bootstrap "gui/$(id -u)" "$plist"
36
+ echo "Installed. The monitor runs at $URL and starts at login."
37
+ }
38
+
39
+ install_linux() {
40
+ command -v systemctl > /dev/null || { echo "systemd is needed: systemctl is not on PATH."; exit 1; }
41
+ local unit_dir=$HOME/.config/systemd/user
42
+ mkdir -p "$unit_dir"
43
+ cat > "$unit_dir/whose-turn.service" <<UNIT
44
+ [Unit]
45
+ Description=Whose Turn monitor
46
+
47
+ [Service]
48
+ ExecStart="$NODE" "$DIR/server.mjs"
49
+ WorkingDirectory=$DIR
50
+ Environment=PATH=$NODE_DIR:/usr/local/bin:/usr/bin:/bin
51
+ Restart=on-failure
52
+
53
+ [Install]
54
+ WantedBy=default.target
55
+ UNIT
56
+ systemctl --user daemon-reload
57
+ systemctl --user enable --now whose-turn
58
+ echo "Installed. The monitor runs at $URL and starts at login. Logs: journalctl --user -u whose-turn"
59
+ }
60
+
61
+ case "$(uname -s)" in
62
+ Darwin) install_macos ;;
63
+ Linux) install_linux ;;
64
+ *)
65
+ echo "No installer for this platform. On Windows, run this once in a terminal, with your node.exe and this folder:"
66
+ echo ' schtasks /Create /SC ONLOGON /TN whose-turn /TR "\"<path to node.exe>\" \"<this folder>\server.mjs\""'
67
+ exit 1
68
+ ;;
69
+ esac
package/lib/api.mjs ADDED
@@ -0,0 +1,406 @@
1
+ /** The local HTTP API and the window's static files; every error leaves as the google.rpc.Status envelope from lib/errors.mjs. */
2
+ import { createServer } from 'node:http';
3
+ import { join } from 'node:path';
4
+ import { createStatic } from './static.mjs';
5
+ import { ApiError, toApiError } from './errors.mjs';
6
+ import { connectorCatalogue, secretTarget, secretsRequired } from '../connectors/index.mjs';
7
+ import { entryShapeViolations, normaliseWorkspaces, WORKSPACE_FIELDS } from './config.mjs';
8
+ import { FACTS } from './facts.mjs';
9
+ import { createFeedback, isConfigured } from './feedback.mjs';
10
+ import { composeIssue, DEFAULT_COMPOSER } from './compose.mjs';
11
+ import { readSize, saveSize, checkMode, spawnOpener } from './window.mjs';
12
+ import { frameHandOff } from './handoff.mjs';
13
+ import { noUpdates } from './update.mjs';
14
+
15
+ const send = (response, code, body, contentType = 'application/json', cacheControl = 'no-store') => {
16
+ const data = Buffer.isBuffer(body) ? body : Buffer.from(contentType === 'application/json' ? JSON.stringify(body) : body);
17
+ response.writeHead(code, { 'Content-Type': `${contentType}; charset=utf-8`, 'Cache-Control': cacheControl, 'Content-Length': data.length });
18
+ response.end(data);
19
+ };
20
+
21
+ const readBody = (request) => new Promise((resolve, reject) => {
22
+ const chunks = [];
23
+ request.on('data', (chunk) => chunks.push(chunk));
24
+ request.on('end', () => {
25
+ try {
26
+ resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {});
27
+ } catch {
28
+ reject(new ApiError('INVALID_BODY'));
29
+ }
30
+ });
31
+ request.on('error', reject);
32
+ });
33
+
34
+ const assertLocal = (request, port) => {
35
+ const allowedHosts = [`127.0.0.1:${port}`, `localhost:${port}`];
36
+ if (!allowedHosts.includes(request.headers.host || '')) {
37
+ throw new ApiError('FORBIDDEN_ORIGIN', { host: request.headers.host || '' });
38
+ }
39
+ const origin = request.headers.origin;
40
+ if (origin && !allowedHosts.map((host) => `http://${host}`).includes(origin)) {
41
+ throw new ApiError('FORBIDDEN_ORIGIN', { origin });
42
+ }
43
+ if (request.method === 'POST' && !/^application\/json/.test(request.headers['content-type'] || '')) {
44
+ throw new ApiError('INVALID_BODY', { content_type: request.headers['content-type'] || '' });
45
+ }
46
+ };
47
+
48
+ const CLOCK_SKEW_MS = 60 * 1000;
49
+
50
+ /** Names the first wrong field in a marks write, or returns '' when the body is fine. */
51
+ const marksProblem = (body) => {
52
+ if (body.seen !== undefined && !(Array.isArray(body.seen) && body.seen.every((id) => typeof id === 'string'))) {
53
+ return 'seen must be a list of strings';
54
+ }
55
+ if ('base' in body && !(Number.isFinite(body.base) && body.base <= Date.now() + CLOCK_SKEW_MS)) {
56
+ return 'base must be a number of milliseconds, not in the future';
57
+ }
58
+ if (body.handled === undefined) {
59
+ return '';
60
+ }
61
+ if (!body.handled || typeof body.handled !== 'object' || Array.isArray(body.handled)) {
62
+ return 'handled must map row ids to an ISO date or null';
63
+ }
64
+ const wrong = Object.entries(body.handled).find(([, value]) => value !== null && !(typeof value === 'string' && Number.isFinite(Date.parse(value))));
65
+ return wrong ? `handled.${wrong[0]} must be an ISO date or null` : '';
66
+ };
67
+
68
+ const aggregateSummary = (workspaces) => {
69
+ const parts = workspaces.map((workspace) => workspace.summary());
70
+ const errors = {};
71
+ for (const part of parts) {
72
+ for (const [source, message] of Object.entries(part.errors)) {
73
+ errors[parts.length > 1 ? `${part.id}.${source}` : source] = message;
74
+ }
75
+ }
76
+ return {
77
+ moves: parts.flatMap((part) => part.moves.map((move) => ({ ...move, ws: part.id }))),
78
+ new_events: parts.reduce((sum, part) => sum + part.new_events, 0),
79
+ todos: parts.flatMap((part) => part.todos.map((todo) => ({ ...todo, ws: part.id }))),
80
+ polled_at: parts.map((part) => part.polled_at).filter(Boolean).sort()[0] || '',
81
+ errors,
82
+ health: {
83
+ ok: parts.every((part) => part.health.ok),
84
+ alerts: parts.flatMap((part) => part.health.alerts.map((alert) => (parts.length > 1 ? `${part.name}: ${alert.text}` : alert.text))),
85
+ },
86
+ workspaces: parts,
87
+ };
88
+ };
89
+
90
+ const OPEN_COOLDOWN_MS = 5000;
91
+
92
+ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig, secrets, inbox, http, reports, dataDir, opener = spawnOpener(appDir), updater = noUpdates() }) => {
93
+ const windowFile = createStatic(join(appDir, 'dist'));
94
+ const feedbackCache = {};
95
+ const pick = (id) => {
96
+ const workspaces = registry.workspaces;
97
+ if (!workspaces.length) {
98
+ throw new ApiError('NO_WORKSPACE');
99
+ }
100
+ if (!id) {
101
+ return workspaces[0];
102
+ }
103
+ const found = workspaces.find((workspace) => workspace.id === id);
104
+ if (!found) {
105
+ throw new ApiError('NO_SUCH_WORKSPACE', { ws: id });
106
+ }
107
+ return found;
108
+ };
109
+ // README.md § API; RMA-115
110
+ const pickForWrite = (id) => {
111
+ if (!id) {
112
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'ws names the workspace this write belongs to' });
113
+ }
114
+ return pick(id);
115
+ };
116
+
117
+ const stream = (response) => {
118
+ response.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store' });
119
+ const write = (event) => response.write(`data: ${JSON.stringify(event)}\n\n`);
120
+ for (const workspace of registry.workspaces) {
121
+ write({ ws: workspace.id, version: workspace.store.state.version, polled_at: workspace.store.state.polled_at });
122
+ }
123
+ write({ update: updater.status() });
124
+ const unsubscribe = registry.subscribe(write);
125
+ const unwatch = updater.subscribe(write);
126
+ const keepAlive = setInterval(() => response.write(': keep-alive\n\n'), 25000);
127
+ response.on('close', () => {
128
+ clearInterval(keepAlive);
129
+ unsubscribe();
130
+ unwatch();
131
+ });
132
+ };
133
+
134
+ const handleGet = async (url, request, response) => {
135
+ const file = url.pathname.startsWith('/api/') ? null : await windowFile(url.pathname);
136
+ if (file) {
137
+ return send(response, 200, file.body, file.type, file.cacheControl);
138
+ }
139
+ if (url.pathname === '/api/workspaces') {
140
+ return send(response, 200, registry.workspaces.map((workspace) => workspace.describe()));
141
+ }
142
+ if (url.pathname === '/api/state') {
143
+ if (!registry.workspaces.length) {
144
+ return send(response, 200, { workspaces: [], problems: registry.problems, update: updater.status() });
145
+ }
146
+ const workspace = pick(url.searchParams.get('ws'));
147
+ return send(response, 200, { ...workspace.snapshot(url.searchParams.get('full') === '1'), workspaces: registry.workspaces.map((item) => item.describe()), problems: registry.problems, update: updater.status() });
148
+ }
149
+ if (url.pathname === '/api/summary') {
150
+ return send(response, 200, { ...aggregateSummary(registry.workspaces), problems: registry.problems, handed: inbox.pending().length, update: updater.status() });
151
+ }
152
+ if (url.pathname === '/api/config') {
153
+ const tables = Object.fromEntries(registry.workspaces.map((workspace) => [workspace.id, workspace.rulesTable]));
154
+ return send(response, 200, { workspaces: registry.rawWorkspaces, problems: registry.problems, connectors: connectorCatalogue(), fields: WORKSPACE_FIELDS, facts: FACTS, tables, config_file: registry.configFile });
155
+ }
156
+ if (url.pathname === '/api/inbox') {
157
+ return send(response, 200, { items: inbox.pending(url.searchParams.get('consumer') || '') });
158
+ }
159
+ if (url.pathname === '/api/feedback') {
160
+ const id = url.searchParams.get('id');
161
+ if (id) {
162
+ return send(response, 200, { report: await reports.detail(reportDeps(), id) });
163
+ }
164
+ const list = await reports.states(reportDeps());
165
+ return send(response, 200, { configured: isConfigured(registry.feedback), project: registry.feedback ? registry.feedback.project : '', reports: list });
166
+ }
167
+ if (url.pathname === '/api/handoff') {
168
+ if (url.searchParams.get('all') === '1') {
169
+ return send(response, 200, frameHandOff('all workspaces', registry.workspaces.map((workspace) => workspace.handOffBody('workspace', workspace.id))), 'text/markdown');
170
+ }
171
+ const workspace = pick(url.searchParams.get('ws'));
172
+ const kind = url.searchParams.get('kind') || 'workspace';
173
+ const text = workspace.handOffText(kind, kind === 'workspace' ? workspace.id : url.searchParams.get('ref') || '');
174
+ if (!text) {
175
+ throw new ApiError('NOT_FOUND', { kind, ref: url.searchParams.get('ref') || '' });
176
+ }
177
+ return send(response, 200, text, 'text/markdown');
178
+ }
179
+ if (url.pathname === '/api/window') {
180
+ return send(response, 200, readSize(dataDir));
181
+ }
182
+ if (url.pathname === '/api/stream') {
183
+ return stream(response);
184
+ }
185
+ throw new ApiError('NOT_FOUND', { path: url.pathname });
186
+ };
187
+
188
+ const handleTodos = (workspace, body) => {
189
+ if (body.action === 'add') {
190
+ if (!String(body.text || '').trim()) {
191
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'text is empty' }, [{ field: 'text', description: 'A to-do needs some text.' }]);
192
+ }
193
+ workspace.addTodo(body);
194
+ } else {
195
+ const todo = workspace.store.todos.find((item) => item.id === body.id);
196
+ if (!todo) {
197
+ throw new ApiError('NO_SUCH_TODO', { id: body.id || '' });
198
+ }
199
+ if (!workspace.changeTodo(todo, body)) {
200
+ throw new ApiError('UNKNOWN_ACTION', { action: body.action || '' });
201
+ }
202
+ }
203
+ workspace.saveTodos();
204
+ return workspace.store.todos;
205
+ };
206
+
207
+ const resolveSecrets = (workspaces, pending) => pending.map((item) => {
208
+ const workspace = workspaces.find((candidate) => candidate.id === item.ws);
209
+ const entry = workspace?.connectors.find((candidate) => candidate.id === item.connector);
210
+ const target = entry ? secretTarget(entry, item.key) : null;
211
+ if (!target || !String(item.value || '').trim()) {
212
+ throw new ApiError('INVALID_ARGUMENT', { detail: `no secret field ${item.key || ''} on connector ${item.connector || ''} of workspace ${item.ws || ''}, or the account for it is empty` });
213
+ }
214
+ return { ...target, value: String(item.value).trim() };
215
+ });
216
+
217
+ const handleConfig = async (body) => {
218
+ if (!Array.isArray(body.workspaces)) {
219
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'workspaces must be a list' });
220
+ }
221
+ if (!body.workspaces.length) {
222
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'keep at least one workspace' });
223
+ }
224
+ const shape = entryShapeViolations(body.workspaces);
225
+ if (shape.length) {
226
+ throw new ApiError('INVALID_CONFIG', { count: String(shape.length) }, shape);
227
+ }
228
+ const { workspaces, problems } = normaliseWorkspaces(body.workspaces);
229
+ if (problems.length) {
230
+ throw new ApiError('INVALID_CONFIG', { count: String(problems.length) }, problems);
231
+ }
232
+ const stored = resolveSecrets(workspaces, Array.isArray(body.secrets) ? body.secrets : []);
233
+ const currentEntries = normaliseWorkspaces(registry.rawWorkspaces).workspaces.flatMap((workspace) => workspace.connectors);
234
+ const missing = [];
235
+ for (const workspace of workspaces) {
236
+ for (const entry of workspace.connectors) {
237
+ for (const key of secretsRequired(currentEntries, entry)) {
238
+ const supplied = (body.secrets || []).some((item) => item.ws === workspace.id && item.connector === entry.id && item.key === key && String(item.value || '').trim());
239
+ if (!supplied) {
240
+ missing.push(`Workspace "${workspace.id}", connector "${entry.id}": the ${key} must be entered again because its account, store entry or endpoint changed.`);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ if (missing.length) {
246
+ throw new ApiError('INVALID_CONFIG', { count: String(missing.length) }, missing);
247
+ }
248
+ for (const secret of stored) {
249
+ await secrets.set(secret.service, secret.account, secret.value);
250
+ }
251
+ await saveConfig(body.workspaces);
252
+ return { workspaces: registry.workspaces.map((workspace) => workspace.describe()), problems: registry.problems, secrets_stored: stored.map(({ service, account }) => ({ service, account })) };
253
+ };
254
+
255
+ const handleSecret = async (body) => {
256
+ const service = String(body.service || '').trim();
257
+ const account = String(body.account || '').trim();
258
+ const value = String(body.value || '').trim();
259
+ if (!/^[\w.-]{1,80}$/.test(service) || !account || !value) {
260
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'service, account and value are required' });
261
+ }
262
+ await secrets.set(service, account, value);
263
+ return { ok: true, service, account };
264
+ };
265
+
266
+ const withContext = (item) => {
267
+ const workspace = registry.workspaces.find((candidate) => candidate.id === item.ws);
268
+ return { ...item, context: workspace ? workspace.handOff(item.kind, item.ref) : null };
269
+ };
270
+
271
+ const handleInbox = (body) => {
272
+ if (body.action === 'clear') {
273
+ inbox.clear();
274
+ return { items: [] };
275
+ }
276
+ if (body.action === 'take') {
277
+ const consumer = String(body.consumer || '').trim();
278
+ if (!consumer) {
279
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'consumer names who takes the items' });
280
+ }
281
+ return { items: inbox.take(consumer).map(withContext) };
282
+ }
283
+ const workspace = pickForWrite(body.ws);
284
+ const kind = String(body.kind || 'workspace');
285
+ const ref = kind === 'workspace' ? workspace.id : String(body.ref || '');
286
+ const snapshot = workspace.handOffSnapshot(kind, ref);
287
+ if (!snapshot) {
288
+ throw new ApiError('NOT_FOUND', { kind, ref });
289
+ }
290
+ return { item: inbox.add({ ws: workspace.id, ws_name: workspace.name, kind, ref, snapshot }) };
291
+ };
292
+
293
+ const reportDeps = () => ({ settings: registry.feedback, secrets, http });
294
+
295
+ let lastRemark = { text: '', at: 0 };
296
+ const handleFeedback = async (body) => {
297
+ if (!isConfigured(registry.feedback)) {
298
+ throw new ApiError('NOT_CONFIGURED');
299
+ }
300
+ const remark = typeof body.text === 'string' ? body.text.trim() : '';
301
+ if (!remark && !String(body.title || '').trim()) {
302
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'text is empty' }, [{ field: 'text', description: 'Write the remark, or pass a title.' }]);
303
+ }
304
+ if (remark && remark === lastRemark.text && Date.now() - lastRemark.at < 60_000) {
305
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'the same remark was just sent' });
306
+ }
307
+ if (remark) {
308
+ lastRemark = { text: remark, at: Date.now() };
309
+ }
310
+ let context = null;
311
+ if (body.ws && body.kind_ref) {
312
+ const workspace = pick(body.ws);
313
+ context = workspace.handOffText(body.kind_ref.kind, body.kind_ref.ref);
314
+ }
315
+ try {
316
+ const composer = registry.feedback && registry.feedback.composer && registry.feedback.composer.command ? registry.feedback.composer : DEFAULT_COMPOSER;
317
+ const shaped = remark ? await composeIssue(remark, { about: context || '', composer, bugWords: registry.feedback && registry.feedback.bug_words }) : { kind: body.kind, title: body.title, body: body.body, composed_by: body.from || 'caller' };
318
+ const issue = await createFeedback({ settings: registry.feedback, secrets, http, cache: feedbackCache, reports }, { kind: shaped.kind, title: shaped.title, body: shaped.body, from: body.from, context: context || body.context || null });
319
+ return { issue, composed_by: shaped.composed_by, reports: await reports.states(reportDeps()) };
320
+ } catch (error) {
321
+ lastRemark = { text: '', at: 0 };
322
+ throw error;
323
+ }
324
+ };
325
+
326
+ const lastOpen = new Map();
327
+
328
+ const handlePost = async (url, request, response) => {
329
+ const body = await readBody(request);
330
+ if (url.pathname === '/api/feedback') {
331
+ return send(response, 200, await handleFeedback(body));
332
+ }
333
+ if (url.pathname === '/api/inbox') {
334
+ return send(response, 200, handleInbox(body));
335
+ }
336
+ if (url.pathname === '/api/config') {
337
+ return send(response, 200, await handleConfig(body));
338
+ }
339
+ if (url.pathname === '/api/secrets') {
340
+ return send(response, 200, await handleSecret(body));
341
+ }
342
+ if (url.pathname === '/api/window') {
343
+ return send(response, 200, saveSize(dataDir, body));
344
+ }
345
+ if (url.pathname === '/api/open') {
346
+ const mode = checkMode(body.mode);
347
+ const wait = OPEN_COOLDOWN_MS - (Date.now() - (lastOpen.get(mode) || -Infinity));
348
+ if (wait > 0) {
349
+ throw new ApiError('OPEN_TOO_SOON', { retry_after_seconds: Math.ceil(wait / 1000) });
350
+ }
351
+ lastOpen.set(mode, Date.now());
352
+ opener({ mode, port });
353
+ return send(response, 200, { ok: true });
354
+ }
355
+ if (url.pathname === '/api/update') {
356
+ return send(response, 200, { update: updater.apply() });
357
+ }
358
+ if (url.pathname === '/api/refresh') {
359
+ requestPoll();
360
+ return send(response, 200, { ok: true });
361
+ }
362
+ const workspace = pickForWrite(body.ws);
363
+ if (url.pathname === '/api/marks') {
364
+ const detail = marksProblem(body);
365
+ if (detail) {
366
+ throw new ApiError('INVALID_ARGUMENT', { detail });
367
+ }
368
+ return send(response, 200, workspace.updateMarks(body));
369
+ }
370
+ if (url.pathname === '/api/todos') {
371
+ return send(response, 200, handleTodos(workspace, body));
372
+ }
373
+ throw new ApiError('NOT_FOUND', { path: url.pathname });
374
+ };
375
+
376
+ const server = createServer(async (request, response) => {
377
+ const url = new URL(request.url, `http://127.0.0.1:${port}`);
378
+ try {
379
+ assertLocal(request, port);
380
+ if (request.method === 'GET') {
381
+ await handleGet(url, request, response);
382
+ } else if (request.method === 'POST') {
383
+ await handlePost(url, request, response);
384
+ } else {
385
+ response.setHeader('Allow', 'GET, POST');
386
+ throw new ApiError('METHOD_NOT_ALLOWED', { method: request.method || '' });
387
+ }
388
+ } catch (error) {
389
+ if (!(error instanceof ApiError)) {
390
+ console.error(error);
391
+ }
392
+ const apiError = toApiError(error);
393
+ send(response, apiError.code, apiError.toEnvelope());
394
+ }
395
+ });
396
+ await new Promise((resolve, reject) => {
397
+ const refuse = (error) => reject(error.code === 'EADDRINUSE' ? new Error(`Port ${port} is already in use: stop the server holding it, or set WHOSE_TURN_PORT to a free port.`, { cause: error }) : error);
398
+ server.once('error', refuse);
399
+ server.listen(port, '127.0.0.1', () => {
400
+ server.off('error', refuse);
401
+ resolve(undefined);
402
+ });
403
+ });
404
+ port = /** @type {import('node:net').AddressInfo} */ (server.address()).port;
405
+ return server;
406
+ };
@@ -0,0 +1,2 @@
1
+ /** The next poll delay in seconds: doubled up to 15 minutes while any source is rate limited, else the normal interval. */
2
+ export const nextDelay = (outcomes, delay, pollSeconds) => (outcomes.some((outcome) => outcome.rateLimited) ? Math.min(delay * 2, 15 * 60) : pollSeconds);
@@ -0,0 +1,67 @@
1
+ /** Turns a remark into an issue (title, kind, body) with the user's own agent CLI, `claude -p` unless the config names another; falls back to the first line as the title. RMA-109. */
2
+ import { execFile } from 'node:child_process';
3
+ import { escapeRegex } from './text.mjs';
4
+ import { untrustedBlock } from './handoff.mjs';
5
+
6
+ const KINDS = ['bug', 'improvement', 'idea'];
7
+ export const DEFAULT_COMPOSER = { command: 'claude', args: ['-p', '--output-format', 'json', '--model', 'claude-haiku-5-5'] };
8
+
9
+ const PROMPT = (text, about) => `You turn a user's remark about a tool called Whose Turn (a local monitor of tickets and pull requests) into an issue for its backlog.
10
+ Answer with one JSON object only, no prose, no code fence: {"title": string, "kind": "bug" | "improvement" | "idea", "body": string}.
11
+ Rules: the title and the body are in English whatever language the remark is in; translate faithfully, keep every detail the user gave, add nothing the user did not say. The title is one line under 80 characters; kind is bug when something misbehaves, improvement when something that exists should be better, idea when it is new; the body restates the remark clearly and may end with one line "Context: …" taken from the context below if it helps a developer find the place, and, when the remark was not in English, with one line "Original: …" quoting it as written.
12
+ Remark:
13
+ ${text}
14
+ ${about ? `\nContext of the window at that moment:\n${untrustedBlock(about.slice(0, 3000))}` : ''}`;
15
+
16
+ const DEFAULT_BUG_WORDS = ['bug', 'broken', 'crash', 'error', 'fail', 'wrong'];
17
+ const fallback = (text, bugWords) => {
18
+ const [first, ...rest] = String(text).trim().split('\n');
19
+ const words = [...DEFAULT_BUG_WORDS, ...[].concat(bugWords || [])].map(escapeRegex).join('|');
20
+ const kind = new RegExp(`(^|[^\\p{L}\\p{N}])(${words})`, 'iu').test(text) ? 'bug' : 'improvement';
21
+ return { title: first.trim().slice(0, 120), kind, body: rest.join('\n').trim(), composed_by: 'fallback' };
22
+ };
23
+
24
+ const issueFrom = (text) => {
25
+ const match = /\{[\s\S]*\}/.exec(text);
26
+ const issue = JSON.parse(match ? match[0] : text);
27
+ if (!issue.title || !KINDS.includes(issue.kind)) {
28
+ throw new Error('incomplete answer');
29
+ }
30
+ return { title: String(issue.title).slice(0, 120), kind: issue.kind, body: String(issue.body || '') };
31
+ };
32
+
33
+ const parse = (output, label) => {
34
+ let text = output;
35
+ try {
36
+ const payload = JSON.parse(output);
37
+ if (payload && typeof payload.result === 'string') {
38
+ text = payload.result;
39
+ } else if (payload && payload.title) {
40
+ return { ...issueFrom(output), composed_by: label };
41
+ }
42
+ } catch {
43
+ /* not a wrapper; the output is the answer itself */
44
+ }
45
+ return { ...issueFrom(text), composed_by: label };
46
+ };
47
+
48
+ const runCommand = (composer) => (prompt, timeoutMs) => new Promise((resolve, reject) => {
49
+ const child = execFile(composer.command, composer.args || [], { timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (error, stdout) => (error ? reject(error) : resolve(stdout)));
50
+ child.stdin.end(prompt);
51
+ });
52
+
53
+ /** @param {string} text @param {{ about?: string, composer?: object, bugWords?: string[], run?: Function, timeoutMs?: number }} [options] */
54
+ export const composeIssue = async (text, { about = '', composer = DEFAULT_COMPOSER, bugWords = [], run, timeoutMs = 45000 } = {}) => {
55
+ const clean = String(text || '').trim();
56
+ if (!clean) {
57
+ throw new Error('Write at least one line');
58
+ }
59
+ const args = composer.args || [];
60
+ const model = args[args.indexOf('--model') + 1];
61
+ const label = composer.label || (args.includes('--model') && model ? `${composer.command} ${model}` : composer.command);
62
+ try {
63
+ return parse(await (run || runCommand(composer))(PROMPT(clean, about), timeoutMs), label);
64
+ } catch {
65
+ return fallback(clean, bugWords);
66
+ }
67
+ };