@xauyxau/hnch 1.1.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.
- package/LICENSE.md +35 -0
- package/README.md +24 -0
- package/dist/index.js +5 -0
- package/package.json +67 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 xau. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software ("hnch") and all associated source code, build artifacts, documentation, and related materials (collectively, the "Software") are proprietary to the copyright holder.
|
|
6
|
+
|
|
7
|
+
## Grant
|
|
8
|
+
|
|
9
|
+
Subject to your acceptance of and compliance with these terms, you are granted a limited, non-exclusive, non-transferable, revocable license to install and run the Software for your personal or internal organizational use only.
|
|
10
|
+
|
|
11
|
+
## Restrictions
|
|
12
|
+
|
|
13
|
+
You may NOT:
|
|
14
|
+
|
|
15
|
+
1. Redistribute, sublicense, sell, lease, rent, or otherwise transfer the Software or any portion of it to any third party.
|
|
16
|
+
2. Modify, adapt, translate, reverse-engineer, decompile, or disassemble the Software, or create derivative works based on the Software, except to the minimum extent expressly permitted by applicable law notwithstanding this limitation.
|
|
17
|
+
3. Remove or alter any copyright, trademark, or other proprietary notices in the Software.
|
|
18
|
+
4. Use the Software to provide a competing service or product.
|
|
19
|
+
5. Use the Software for any unlawful purpose.
|
|
20
|
+
|
|
21
|
+
## No Warranty
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
24
|
+
|
|
25
|
+
## Limitation of Liability
|
|
26
|
+
|
|
27
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
28
|
+
|
|
29
|
+
## Termination
|
|
30
|
+
|
|
31
|
+
This license terminates automatically if you fail to comply with any of its terms. Upon termination you must cease all use of the Software and destroy all copies in your possession.
|
|
32
|
+
|
|
33
|
+
## Severability
|
|
34
|
+
|
|
35
|
+
If any provision of these terms is held to be unenforceable, the remaining provisions will remain in full force and effect.
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# hnch — pointing poker CLI
|
|
2
|
+
|
|
3
|
+
Terminal client for hnch (pointing poker for agile teams). Speaks the same Yjs-over-WebSocket protocol as the mobile/PWA client.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
npm i -g @xauyxau/hnch
|
|
8
|
+
# or one-shot
|
|
9
|
+
npx @xauyxau/hnch create
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
hnch create # create a room, drop into the session as host
|
|
14
|
+
hnch join ABC123 # join an existing room by 6-char code
|
|
15
|
+
hnch resume # rejoin your last session
|
|
16
|
+
hnch config # configure default scale / display name / relay URL
|
|
17
|
+
|
|
18
|
+
## Default relay
|
|
19
|
+
|
|
20
|
+
`wss://hnch.dev`. Override with the `HNCH_RELAY_URL` env var or `hnch config --relay-url <url>`.
|
|
21
|
+
|
|
22
|
+
## License
|
|
23
|
+
|
|
24
|
+
Proprietary. See [LICENSE.md](https://github.com/IIxauII/hnch/blob/main/LICENSE.md).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import ds from'yargs';import {hideBin}from'yargs/helpers';import nt,{useState,useEffect,useCallback,useMemo}from'react';import {render,useApp,useInput,Box,Text}from'ink';import*as he from'yjs';import {Awareness}from'y-protocols/awareness';import {mkdirSync,chmodSync,writeFileSync,unlinkSync,existsSync,readFileSync}from'fs';import {homedir}from'os';import {join}from'path';import {randomUUID}from'crypto';import {createInterface}from'readline';import {WebsocketProvider}from'y-websocket';import Co from'ws';import {jsxs,jsx,Fragment}from'react/jsx-runtime';var no="wss://hnch.dev";function be(){let t=process.env.XDG_CONFIG_HOME||join(homedir(),".config");return join(t,"hnch")}function it(){return join(be(),"config.json")}function Dt(){let e=be();existsSync(e)||mkdirSync(e,{recursive:true});}function re(){let e=it();if(!existsSync(e))return {};try{return JSON.parse(readFileSync(e,"utf-8"))}catch{return {}}}function V(e){Dt();let n={...re(),...e};writeFileSync(it(),JSON.stringify(n,null,2)+`
|
|
3
|
+
`,"utf-8");}async function oo(e){let t=createInterface({input:process.stdin,output:process.stderr});return new Promise(n=>{t.question(e,o=>{t.close(),n(o.trim());});})}async function Re(){let e=re(),t=false;if(e.participantId||(e.participantId=randomUUID(),t=true),!e.displayName){let o=await oo("Enter your display name: ");e.displayName=o||`user-${e.participantId.slice(0,6)}`,t=true;}e.relayUrl||(e.relayUrl=no,t=true),t&&V(e);let n=process.env.HNCH_RELAY_URL||e.relayUrl;return {participantId:e.participantId,displayName:e.displayName,relayUrl:n,defaultScale:e.defaultScale,defaultSessionName:e.defaultSessionName,defaultPassword:e.defaultPassword,presetOverrides:e.presetOverrides,customScales:e.customScales}}function Ie(e){Dt();let n={...re()};for(let[o,s]of Object.entries(e))s===""?delete n[o]:s!==void 0&&(n[o]=s);writeFileSync(it(),JSON.stringify(n,null,2)+`
|
|
4
|
+
`,"utf-8");}var so={"unknown-room":4001,"wrong-password":4002,"invalid-invite":4003,"expired-invite":4004,"invite-already-redeemed":4005,"room-full":4006,"malformed-credentials":4010},Mt=new Map(Object.entries(so).map(([e,t])=>[t,e]));function ie(e){return e.role??"voter"}var je={id:"fibonacci",cards:["0","1","2","3","5","8","13","21"]},Ut={id:"t-shirt",cards:["XS","S","M","L","XL"]},Bt={id:"powers-of-2",cards:["1","2","4","8","16","32","64"]},Ht={id:"linear",cards:["1","2","3","4","5","6","7","8","9","10"]},jt={id:"risk",cards:["Low","Medium","High","Critical"]},Q={[je.id]:je,[Ut.id]:Ut,[Bt.id]:Bt,[Ht.id]:Ht,[jt.id]:jt},Lt={fibonacci:"Fibonacci","t-shirt":"T-shirt","powers-of-2":"Powers of 2",linear:"Linear (1\u201310)",risk:"Risk"};function ae(e){return e in Lt?Lt[e]:e.length===0?e:(e.startsWith("custom-")?e.slice(7):e).split("-").map(n=>n.length===0?n:n.charAt(0).toUpperCase()+n.slice(1)).join(" ")}var ro="meta",Te="settings",io="participants",ao="rounds",co=["roomUuid","createdAt","createdBy","sessionEndedAt"],lo=["name","defaultScale","hostParticipantId"],uo=["displayName","joinedAt","role"],po=["id","title","description","ticketId","parentRoundId","scale","timerStartedAt","revealedAt","createdBy","createdAt"];function at(e){return e.getMap(ro)}function Le(e){return e.getMap(Te)}function ke(e){return e.getMap(io)}function ct(e){return e.getArray(ao)}function _e(e,t){let n={};for(let o of t){let s=e.get(o);s!==void 0&&(n[o]=s);}return n}function mo(e){return _e(at(e),co)}function fo(e){return _e(Le(e),lo)}function go(e){let t=ke(e),n=[];return t.forEach((o,s)=>{let r=_e(o,uo);n.push({id:s,...r});}),n.sort((o,s)=>o.joinedAt-s.joinedAt||o.id.localeCompare(s.id)),n}function ho(e){let t={};return e.forEach((n,o)=>{t[o]=n;}),t}function So(e){let t=_e(e,po),n=e.get("votes"),o=n?ho(n):{};return {...t,votes:o}}function wo(e){let t=ct(e),n=[];return t.forEach(o=>{n.push(So(o));}),n}function G(e){return {meta:mo(e),settings:fo(e),participants:go(e),rounds:wo(e)}}function kt(e,t){if(at(e).has("roomUuid"))throw new Error("initDoc: doc already seeded");e.transact(()=>{let n=at(e);n.set("roomUuid",t.roomUuid),n.set("createdAt",t.createdAt),n.set("createdBy",t.participantId);let o=Le(e);t.sessionName!==void 0&&o.set("name",t.sessionName),o.set("defaultScale",t.defaultScale),o.set("hostParticipantId",t.participantId);let s=new he.Map;s.set("displayName",t.displayName),s.set("joinedAt",t.createdAt),ke(e).set(t.participantId,s);});}function $e(e,t){e.transact(()=>{let n=new he.Map;n.set("displayName",t.displayName),n.set("joinedAt",Date.now()),ke(e).set(t.participantId,n);});}function Se(e,t){e.transact(()=>{let n=Le(e);for(let[o,s]of Object.entries(t))s===void 0?n.delete(o):n.set(o,s);});}function dt(e,t,n){e.transact(()=>{let o=ke(e),s=o.get(t),r=s??new he.Map;r.set("displayName",n.displayName),r.set("joinedAt",n.joinedAt),n.role!==void 0&&r.set("role",n.role),s===void 0&&o.set(t,r);});}function lt(e,t){let n=t.scale??Le(e).get("defaultScale");if(!n)throw new Error("appendRound: no scale (settings.defaultScale missing)");return e.transact(()=>{let o=new he.Map;o.set("id",t.id),o.set("createdAt",t.createdAt),o.set("createdBy",t.createdBy),o.set("scale",n),t.title!==void 0&&o.set("title",t.title),t.description!==void 0&&o.set("description",t.description),t.ticketId!==void 0&&o.set("ticketId",t.ticketId),t.parentRoundId!==void 0&&o.set("parentRoundId",t.parentRoundId),o.set("timerStartedAt",t.createdAt),o.set("votes",new he.Map),ct(e).push([o]);}),t.id}function ut(e,t){let n=ct(e);for(let o=0;o<n.length;o+=1){let s=n.get(o);if(s.get("id")===t)return s}}function _t(e,t,n,o){let s=ut(e,t);if(!s)throw new Error(`castVote: round ${t} not found`);s.get("revealedAt")===void 0&&e.transact(()=>{s.get("votes").set(n,o);});}function $t(e,t,n){let o=ut(e,t);if(!o)throw new Error(`clearVote: round ${t} not found`);o.get("revealedAt")===void 0&&e.transact(()=>{o.get("votes").delete(n);});}function pt(e,t,n){let o=ut(e,t);if(!o)throw new Error(`revealRound: round ${t} not found`);e.transact(()=>{o.set("revealedAt",n);});}function z(e){let{rounds:t}=e;if(t.length===0)return null;for(let n=t.length-1;n>=0;n-=1){let o=t[n];if(o.revealedAt===void 0)return o}return t[t.length-1]}function Yt(e){let t=Object.values(e.votes);return t.length<2?false:t.every(n=>n===t[0])}var Vt="PBKDF2",yo="SHA-256",Ft=new TextEncoder;function xo(e){return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}async function ce(e,t){let n=await crypto.subtle.importKey("raw",Ft.encode(e),Vt,false,["deriveBits"]),o=await crypto.subtle.deriveBits({name:Vt,salt:Ft.encode(t),iterations:1e5,hash:yo},n,256);return xo(o)}var Jt="ABCDEFGHJKMNPQRSTUVWXYZ23456789";if(Jt.length!==31)throw new Error(`Room code alphabet must be 31 characters, got ${Jt.length}`);function Wt(e){return e.trim().replace(/-/g,"").toUpperCase()}function Ye(e,t,n){if(e.startsWith("custom-")){let s=(n??[]).find(r=>r.id===e);return s?{id:s.id,cards:s.cards}:void 0}let o=Q[e];if(o){let s=(t??{})[e]??[];if(s.length===0)return o;let r=o.cards.filter((l,a)=>!s.includes(a));return r.length===0?void 0:{id:e,cards:r}}}function Ve(e,t){let n=Object.keys(Q).map(s=>Ye(s,e,t)).filter(s=>s!==void 0),o=(t??[]).map(s=>({id:s.id,cards:s.cards}));return [...n,...o]}async function qt(e,t){let n=await fetch(`${e}/rooms`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({})});if(!n.ok)throw new Error(`POST /rooms failed: ${n.status}`);return await n.json()}async function Ee(e,t){let n=await fetch(`${e}/rooms/${encodeURIComponent(t)}`);if(n.status===404){let o=await n.json().catch(()=>null);throw new Error(`Room not found: ${o?.reason??"unknown-room"}`)}if(!n.ok)throw new Error(`GET /rooms/:code failed: ${n.status}`);return await n.json()}async function mt(e,t,n){let o=await fetch(`${e}/rooms/${t}/access`,{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify({passwordHash:n})});if(!o.ok)throw new Error(`PATCH /rooms/:uuid/access failed: ${o.status}`)}function Gt(e){let n=`${e.wsBaseUrl.replace(/^http/,"ws")}/ws`,o={did:e.deviceId};e.passwordHash!==void 0&&(o.pw=e.passwordHash);let s=new WebsocketProvider(n,e.roomUuid,e.doc,{params:o,connect:true,...e.awareness!==void 0&&{awareness:e.awareness},WebSocketPolyfill:Co}),r=d=>{e.onStatusChange?.(d.status);},l=d=>{if(d===null)return;let i=Mt.get(d.code);i!==void 0&&(e.onRejection?.(i),s.shouldConnect=false,s.disconnect());};s.on("status",r),s.on("connection-close",l);let a=false,u=e.passwordHash;return {doc:e.doc,awareness:s.awareness,updateCredentials(d){if(d.passwordHash===u)return;u=d.passwordHash??void 0,s.shouldConnect=false,s.disconnect();let c={did:s.params.did??""};u!==void 0&&(c.pw=u),s.params=c,s.connect();},disconnect(){a||(a=true,s.off("status",r),s.off("connection-close",l),s.shouldConnect=false,s.disconnect(),s.destroy());}}}var bo=5e3;function Qt(e,t,n){let o=null,s=null;function r(){let i=new Set;return t.getStates().forEach(c=>{let g=c.participantId;typeof g=="string"&&i.add(g);}),i}function l(){return e.getMap(Te).get("hostParticipantId")}function a(){s!==null&&(clearTimeout(s),s=null),o=null;}function u(){let i=l(),c=o;if(o=null,!c||i!==c)return;let g=r();if(g.has(i))return;let T=[...g].sort();if(T.length===0)return;let E=T[0];E===n&&Se(e,{hostParticipantId:E});}function d(){let i=l();if(!i)return;if(r().has(i)){a();return}o!==i&&(a(),o=i,s=setTimeout(()=>{s=null,u();},bo));}return t.on("change",d),e.getMap(Te).observe(d),d(),{stop(){a(),t.off("change",d),e.getMap(Te).unobserve(d);}}}async function Xt(e){let t,n=await qt(e.relayUrl);e.password&&(t=await ce(e.password,n.uuid),await mt(e.relayUrl,n.uuid,t));let o=new he.Doc,s=new Awareness(o),r=Date.now();return kt(o,{roomUuid:n.uuid,createdAt:r,participantId:e.participantId,displayName:e.displayName,defaultScale:e.defaultScale??je,sessionName:e.sessionName}),s.setLocalStateField("participantId",e.participantId),Fe({doc:o,awareness:s,roomCode:n.code,roomUuid:n.uuid,participantId:e.participantId,relayUrl:e.relayUrl,passwordHash:t})}async function Zt(e){let t=await Ee(e.relayUrl,e.roomCode),n;if(t.accessMode==="password"){if(!e.password)throw new Error("Room requires a password");n=await ce(e.password,t.uuid);}let o=new he.Doc,s=new Awareness(o);return $e(o,{participantId:e.participantId,displayName:e.displayName}),s.setLocalStateField("participantId",e.participantId),Fe({doc:o,awareness:s,roomCode:e.roomCode,roomUuid:t.uuid,participantId:e.participantId,relayUrl:e.relayUrl,passwordHash:n,onRejection:e.onRejection})}function Fe(e){let{doc:t,awareness:n,participantId:o,relayUrl:s,roomUuid:r}=e,l=new Set,a=new Set,u=new Set,d=Gt({wsBaseUrl:e.relayUrl,roomUuid:e.roomUuid,doc:t,deviceId:o,passwordHash:e.passwordHash,awareness:n,onStatusChange(p){l.forEach(m=>m(p));},onRejection(p){console.error(`Connection rejected: ${p}`),e.onRejection?.(p);}}),i=Qt(t,n,o),c=()=>{a.forEach(p=>p());};t.getMap("meta").observeDeep(c),t.getMap("settings").observeDeep(c),t.getMap("participants").observeDeep(c),t.getArray("rounds").observeDeep(c);function g(){let p=new Set;return n.getStates().forEach(m=>{let b=m.participantId;typeof b=="string"&&p.add(b);}),p}let T=new Set,E=setInterval(()=>{let p=G(t),m=z(p);if(!m||m.revealedAt!==void 0||T.has(m.id))return;let b=g();if(b.size===0)return;let y=p.participants.filter(f=>b.has(f.id)&&ie(f)==="voter");if(y.length===0)return;y.filter(f=>f.id in m.votes).length>=y.length&&(T.add(m.id),pt(t,m.id,Date.now()));},250),C=t.getMap("settings").get("hostParticipantId"),O=()=>{let p=t.getMap("settings").get("hostParticipantId");p!==C&&p!==void 0&&u.forEach(m=>m(p)),C=p;};t.getMap("settings").observe(O);let U=false;return {doc:t,awareness:n,roomCode:e.roomCode,roomUuid:e.roomUuid,projection(){return G(t)},current(){return z(G(t))},isHost(){return G(t).settings.hostParticipantId===o},onlineIds:g,vote(p){let m=z(G(t));m&&_t(t,m.id,o,p);},unvote(){let p=z(G(t));p&&$t(t,p.id,o);},reveal(){let p=z(G(t));p&&pt(t,p.id,Date.now());},newRound(p){let m=randomUUID();return lt(t,{id:m,createdAt:Date.now(),createdBy:o,title:p}),m},revote(){let p=z(G(t)),m=randomUUID();return lt(t,{id:m,createdAt:Date.now(),createdBy:o,parentRoundId:p?.id,title:p?.title}),m},updateName(p){Se(t,{name:p});},updateScale(p){Se(t,{defaultScale:p});},updateDisplayName(p){let y=G(t).participants.find(A=>A.id===o)?.joinedAt??Date.now();dt(t,o,{displayName:p,joinedAt:y});},async updateAccess(p,m){let b=null;if(p==="password"){if(!m)throw new Error("Password required for password mode");b=await ce(m,r);}await mt(s,r,b),d.updateCredentials({passwordHash:b});},handoffHost(p){if(!this.isHost())throw new Error("Only the current host can hand off");if(!g().has(p))throw new Error("Target participant is not online");if(!G(t).participants.some(A=>A.id===p))throw new Error("Target participant does not exist");Se(t,{hostParticipantId:p});},toggleRole(){let m=G(t).participants.find(f=>f.id===o),y=ie(m??{})==="viewer"?"voter":"viewer",A=m?.joinedAt??Date.now();dt(t,o,{displayName:m?.displayName??"",joinedAt:A,role:y});},onChange(p){return a.add(p),()=>a.delete(p)},onStatusChange(p){return l.add(p),()=>l.delete(p)},onHostChange(p){return u.add(p),()=>u.delete(p)},disconnect(){U||(U=true,clearInterval(E),i.stop(),t.getMap("meta").unobserveDeep(c),t.getMap("settings").unobserveDeep(c),t.getMap("settings").unobserve(O),t.getMap("participants").unobserveDeep(c),t.getArray("rounds").unobserveDeep(c),d.disconnect());}}}function en(e){let t=Wt(e);return t===""?null:t}var Ao=1440*60*1e3;function gt(){return join(be(),"last-session.json")}function Oo(e){if(typeof e!="object"||e===null)return false;let t=e;return typeof t.roomCode=="string"&&typeof t.roomUuid=="string"&&typeof t.lastSeenAt=="number"&&(t.passwordHash===void 0||typeof t.passwordHash=="string")}function nn(){let e=gt();if(!existsSync(e))return null;try{let t=JSON.parse(readFileSync(e,"utf-8"));return !Oo(t)||Date.now()-t.lastSeenAt>Ao?(L(),null):t}catch{return L(),null}}function Je(e){let t=be();mkdirSync(t,{recursive:true,mode:448}),chmodSync(t,448);let n=gt();writeFileSync(n,JSON.stringify(e,null,2)+`
|
|
5
|
+
`,{encoding:"utf-8",mode:384}),chmodSync(n,384);}function L(){let e=gt();try{unlinkSync(e);}catch(t){if(t.code!=="ENOENT")throw t}}async function We(e){let t=true;try{await e.clearLastSession();}catch(n){t=false,e.logger.error(`Failed to clear cached session: ${n instanceof Error?n.message:String(n)}`);}return t?e.logger.error("Session expired (room no longer exists). Cached session cleared."):e.logger.error("Session expired. Could not delete ~/.config/hnch/last-session.json \u2014 please remove it manually."),e.exit(1)}function qe(e){let t=e.doc.getMap("meta"),n=null,o=false;function s(){if(o)return;let r=t.get("sessionEndedAt");if(r==null||r===n)return;n=r;let l=t.get("createdBy"),a=l===e.localParticipantId,u=e.doc.getMap("participants"),d=e.doc.getMap("settings").get("hostParticipantId")??l,i="host";if(d){let g=u.get(d)?.get("displayName");typeof g=="string"&&g.length>0&&(i=g);}setImmediate(()=>{o||e.onEnded({isLocal:a,hostName:i,timestamp:r});});}return t.observe(s),s(),()=>{o=true,t.unobserve(s);}}var Do={connecting:"yellow",connected:"green",disconnected:"red"};function rn({roomCode:e,sessionName:t,status:n,isHost:o}){let s=t?`${e} \xB7 ${t}`:e;return jsxs(Box,{borderStyle:"single",borderColor:"gray",paddingX:1,justifyContent:"space-between",children:[jsxs(Text,{bold:true,children:["hnch \xB7 ",s]}),jsxs(Box,{gap:2,children:[o&&jsx(Text,{color:"yellow",children:"[host]"}),jsx(Text,{color:Do[n],children:n})]})]})}function ze({participants:e,onlineIds:t,hostId:n,votes:o,revealed:s}){return jsxs(Box,{flexDirection:"column",children:[jsxs(Text,{bold:true,children:["Participants (",e.length,")"]}),e.map(r=>{let l=t.has(r.id),a=l?"\u25CF":"\u25CB",u=l?"green":"gray",d=r.id===n,i=ie(r)==="viewer",c=r.id in o,g=i?" (viewer)":s?o[r.id]?` \u2192 ${o[r.id]}`:"":c?" \u2713":" \xB7";return jsxs(Box,{gap:1,children:[jsx(Text,{color:u,children:a}),jsxs(Text,{children:[r.displayName,d?" (host)":""]}),jsx(Text,{color:i?"gray":c?"green":"gray",children:g})]},r.id)})]})}function Ne({items:e,onSelect:t,onClose:n,title:o}){if(e.length===0)return jsxs(Box,{flexDirection:"column",gap:1,children:[o&&jsx(Text,{bold:true,children:o}),jsx(Text,{dimColor:true,children:"(no items)"})]});let s=e.findIndex(a=>!a.disabled),[r,l]=useState(s>=0?s:0);return useInput((a,u)=>{if(u.upArrow)l(d=>{let i=(d-1+e.length)%e.length,c=0;for(;e[i]?.disabled&&c<e.length;)i=(i-1+e.length)%e.length,c++;return i});else if(u.downArrow)l(d=>{let i=(d+1)%e.length,c=0;for(;e[i]?.disabled&&c<e.length;)i=(i+1)%e.length,c++;return i});else if(u.return){let d=e[r];d&&!d.disabled&&t(d.value);}else u.escape&&n?.();}),jsxs(Box,{flexDirection:"column",gap:1,children:[o&&jsx(Text,{bold:true,children:o}),jsx(Box,{flexDirection:"column",children:e.map((a,u)=>{let d=u===r,i=a.disabled??false;return jsxs(Box,{gap:1,children:[jsx(Text,{color:d?"cyan":void 0,dimColor:i,children:d?">":" "}),jsx(Text,{bold:d,dimColor:i,children:a.label}),a.detail&&jsx(Text,{dimColor:true,children:a.detail})]},a.value)})}),jsx(Text,{dimColor:true,children:"\u2191\u2193 select \u23CE confirm esc close"})]})}function W({value:e,onChange:t,onSubmit:n,onCancel:o,placeholder:s,isActive:r=true,mask:l=false}){useInput((i,c)=>{c.escape?o?.():c.return?n(e):c.backspace||c.delete?t(e.slice(0,-1)):i&&!c.ctrl&&!c.meta&&t(e+i);},{isActive:r});let a=l&&e.length>0?"*".repeat(e.length):e,u=a.length>0?a:s??"",d=e.length===0&&s!==void 0;return jsxs(Text,{children:[jsx(Text,{dimColor:d,children:u}),jsx(Text,{inverse:true,children:" "})]})}function pn({mode:e,password:t,onModeChange:n,onPasswordChange:o,onClose:s}){let[r,l]=useState(e==="open"?0:1),[a,u]=useState(false);return useEffect(()=>{l(e==="open"?0:1);},[e]),useInput((d,i)=>{if(i.leftArrow)l(c=>Math.max(0,c-1));else if(i.rightArrow)l(c=>Math.min(1,c+1));else if(i.return){let c=r===0?"open":"password";c!==e&&n(c),c==="password"&&u(true);}else i.escape&&s?.();},{isActive:!a}),jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Box,{gap:1,children:["open","password"].map((d,i)=>{let c=i===r,g=d===e,T=d==="open"?"Open":"Password";return jsx(Text,{inverse:c,bold:g,color:g?"cyan":void 0,children:g?`[${T}]`:` ${T} `},d)})}),e==="password"&&jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{children:"Password: "}),jsx(W,{value:t,onChange:o,onSubmit:()=>u(false),onCancel:()=>u(false),placeholder:"(required to join)",isActive:a,mask:true}),!a&&jsx(Text,{dimColor:true,children:"\u23CE edit \u2190 \u2192 switch mode esc close"}),a&&jsx(Text,{dimColor:true,children:"\u23CE done esc cancel"})]}),e==="open"&&jsx(Text,{dimColor:true,children:"\u2190\u2192 toggle \u23CE confirm esc close"})]})}function gn({scaleId:e,cards:t,disabledIndices:n,onOverrideChange:o,onClose:s}){let r=useMemo(()=>new Set(n),[n]),[l,a]=useState(0),u=d=>{let i=new Set(r);if(i.has(d))i.delete(d);else {if(i.size>=t.length-1)return;i.add(d);}let c=Array.from(i).sort((g,T)=>g-T);o(e,c);};return useInput((d,i)=>{i.leftArrow?a(c=>Math.max(0,c-1)):i.rightArrow?a(c=>Math.min(t.length-1,c+1)):d===" "?u(l):(i.return||i.escape)&&s();}),jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Box,{gap:1,children:t.map((d,i)=>{let c=i===l,g=r.has(i);return jsx(Text,{inverse:c,bold:!g,color:g?void 0:"cyan",strikethrough:g,dimColor:g,children:d},d)})}),jsx(Text,{dimColor:true,children:" \u2190\u2192 navigate space toggle \u23CE/esc done"})]})}function bt({initialName:e="",initialCards:t=[],onSave:n,onCancel:o}){let s=e.length>0,[r,l]=useState(e),[a,u]=useState(t.length>0?t:[""]),[d,i]=useState(0),[c,g]=useState("editing-name"),[T,E]=useState(""),C=()=>{let p=r.trim();if(p.length<1||p.length>40){E("Name must be 1-40 characters");return}l(p),E(""),g("editing-cards");},O=()=>{let p=r.trim();if(p.length<1||p.length>40){E("Name must be 1-40 characters");return}let m=a.map(y=>y.trim()).filter(y=>y.length>0),b=Array.from(new Set(m));if(b.length<2){E("Need at least 2 unique non-empty cards");return}E(""),n(p,b);};return useInput((p,m)=>{if(c==="editing-cards"){if(m.leftArrow)i(b=>Math.max(0,b-1));else if(m.rightArrow)i(b=>{let y=b+1;return y>=a.length&&a.length<25?(u(A=>[...A,""]),y):Math.min(y,a.length-1)});else if(m.return)O();else if(m.escape)o();else if(m.tab)g("editing-name");else if(m.backspace){let b=a[d]||"";if(b.length===0){if(a.length>2){let y=a.filter((A,f)=>f!==d);u(y),i(Math.max(0,d-1));}}else {let y=[...a];y[d]=b.slice(0,-1),u(y);}}else if(p&&!m.ctrl&&!m.meta){let b=a[d]||"";if(b.length<10){let y=[...a];y[d]=b+p,u(y);}}}},{isActive:c==="editing-cards"}),jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:s?"\u2500\u2500 Edit Custom Scale \u2500\u2500":"\u2500\u2500 Create Custom Scale \u2500\u2500"}),jsxs(Box,{flexDirection:"column",gap:0,children:[jsx(Text,{children:"Name:"}),jsx(W,{value:r,onChange:l,onSubmit:C,onCancel:o,placeholder:"Scale name",isActive:c==="editing-name"})]}),c==="editing-cards"&&jsxs(Box,{flexDirection:"column",gap:0,children:[jsx(Text,{children:"Cards:"}),jsx(Box,{gap:1,flexWrap:"wrap",children:a.map((p,m)=>{let b=m===d;return jsx(Text,{inverse:b,color:b?void 0:"cyan",children:p.length>0?p:"\xB7"},m)})})]}),T&&jsx(Text,{color:"red",children:T}),jsx(Text,{dimColor:true,children:c==="editing-name"?"\u23CE next esc cancel":"\u2190\u2192 navigate typing adds card bsp delete \u23CE save tab name esc cancel"})]})}function Ze({scales:e,selectedScaleId:t,presetOverrides:n,customScales:o=[],onSelect:s,onOverrideChange:r,onCustomScaleCreate:l,onCustomScaleEdit:a,onCustomScaleDelete:u,onClose:d,showNoneOption:i=false}){let[c,g]=useState(0),[T,E]=useState(null),[C,O]=useState("browsing"),[U,p]=useState(null),[m,b]=useState(null),y=l?[...e,{id:"__create-custom__",cards:[]}]:e,A=y.length;if(useInput((f,D)=>{if(C==="browsing"){if(D.escape){d();return}if(A===0)return;if(D.upArrow)g(v=>(v-1+A)%A);else if(D.downArrow)g(v=>(v+1)%A);else if(D.return){let v=y[c];if(!v)return;if(v.id==="__create-custom__"){O("creating");return}v.id in Q?E(T===v.id?null:v.id):s(v.id);}else if(f===" "){let v=y[c];v&&v.id!=="__create-custom__"&&s(v.id);}else if(f==="e"||f==="E"){let v=y[c];a&&v?.id.startsWith("custom-")&&(p(v.id),O("editing"));}else if(f==="d"||f==="D"){let v=y[c];u&&v?.id.startsWith("custom-")&&(b(v.id),O("confirming-delete"));}}else C==="confirming-delete"&&(f==="y"||f==="Y"?(m&&u&&u(m),b(null),O("browsing")):(f==="n"||f==="N"||D.escape)&&(b(null),O("browsing")));},{isActive:C==="browsing"&&T===null||C==="confirming-delete"}),C==="creating")return jsx(bt,{onSave:(f,D)=>{l&&l(f,D),O("browsing");},onCancel:()=>O("browsing")});if(C==="editing"&&U){let f=o?.find(v=>v.id===U);if(e.find(v=>v.id===U)&&f)return jsx(bt,{initialName:f.label,initialCards:f.cards,onSave:(v,k)=>{a&&a(U,v,k),p(null),O("browsing");},onCancel:()=>{p(null),O("browsing");}})}if(C==="confirming-delete"&&m){let f=y.find(v=>v.id===m),D=f?ae(f.id):"Unknown";return jsxs(Box,{flexDirection:"column",gap:1,children:[jsxs(Text,{children:["Delete ",D,"? "]}),jsx(Text,{children:"[y] yes [n] no esc cancel"})]})}return y.length===0?jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{dimColor:true,children:"(no scales available)"}),jsx(Text,{dimColor:true,children:"esc back"})]}):jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Box,{flexDirection:"column",children:y.map((f,D)=>{let v=D===c,k=f.id===t,B=T===f.id,Y=f.id in Q;f.id.startsWith("custom-");let j=f.id==="__create-custom__",h=f.cards.join(", "),x=i&&f.id==="",S=x?"(none)":j?"[+ Create custom scale]":ae(f.id);return jsxs(Box,{flexDirection:"column",children:[jsxs(Box,{gap:1,children:[jsx(Text,{color:v?"cyan":void 0,children:v?">":" "}),jsx(Text,{bold:v,children:S}),!x&&!j&&jsx(Text,{dimColor:true,children:h}),k&&jsx(Text,{color:"cyan",children:"[current]"})]}),B&&!x&&!j&&jsx(Box,{paddingLeft:2,children:Y?jsx(gn,{scaleId:f.id,cards:Q[f.id]?.cards??[],disabledIndices:n?.[f.id]??[],onOverrideChange:r,onClose:()=>E(null)}):null})]},f.id||"none")})}),jsx(Text,{dimColor:true,children:(()=>{let f=c<y.length?y[c]:null;return f?.id.startsWith("custom-")?`\u2191\u2193 navigate space select${a?" [e] edit":""}${u?" [d] delete":""} esc back`:f&&f.id in Q?"\u2191\u2193 navigate \u23CE customize space select esc back":"\u2191\u2193 navigate \u23CE/space select esc back"})()})]})}function hn({sessionName:e,displayName:t,currentScale:n,accessMode:o,presetOverrides:s,customScales:r,onChangeName:l,onChangeScale:a,onChangeDisplayName:u,onChangeAccess:d,onClose:i}){let[c,g]=useState(s??{}),T=useMemo(()=>Ve(c,r),[c,r]),[E,C]=useState("main-menu"),[O,U]=useState(e??""),[p,m]=useState(t),[b,y]=useState(o==="password"?"password":"open"),[A,f]=useState(""),[D,v]=useState(false),k=[{label:"Room name",value:"room-name",detail:`[${e??"(none)"}]`},{label:"Scale",value:"scale",detail:`[${ae(n.id)}]`},{label:"Access",value:"access",detail:`[${o==="open"?"Open":"Password"}]`},{label:"Display name",value:"display-name",detail:`[${t}]`}];function B(S){S==="room-name"?(U(e??""),C("editing-room-name")):S==="scale"?C("picking-scale"):S==="access"?C("picking-access"):S==="display-name"&&(m(t),C("editing-display-name"));}function Y(S){let N=S.trim()===""?void 0:S.trim();l(N),C("main-menu");}function _(S){let N=S.trim();N.length>0&&(u(N),process.nextTick(()=>V({displayName:N}))),C("main-menu");}function j(S){let N=T.find(w=>w.id===S);N&&a(N),C("main-menu");}function h(S,N){let w={...c};N.length===0?delete w[S]:w[S]=N,g(w),V({presetOverrides:Object.keys(w).length>0?w:void 0});}async function x(S,N){v(true);try{await d(S,N),y(S),f(S==="password"?N??"":"");}catch(w){console.error(`Failed to update access: ${w}`);}finally{v(false),C("main-menu");}}return E==="main-menu"?jsx(Ne,{title:"\u2500\u2500 Settings \u2500\u2500",items:k,onSelect:B,onClose:i}):E==="editing-room-name"?jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Settings \u2500\u2500"}),jsx(Text,{children:"Room name: "}),jsx(W,{value:O,onChange:U,onSubmit:Y,onCancel:()=>C("main-menu"),placeholder:"(leave empty to remove)",isActive:true}),jsx(Text,{dimColor:true,children:"\u23CE confirm esc back"})]}):E==="editing-display-name"?jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Settings \u2500\u2500"}),jsx(Text,{children:"Display name: "}),jsx(W,{value:p,onChange:m,onSubmit:_,onCancel:()=>C("main-menu"),placeholder:"(required)",isActive:true}),jsx(Text,{dimColor:true,children:"\u23CE confirm esc back"})]}):E==="picking-scale"?jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Settings \u2500\u2500"}),jsx(Ze,{scales:T,selectedScaleId:n.id,presetOverrides:c,onSelect:j,onOverrideChange:h,onClose:()=>C("main-menu")})]}):E==="picking-access"?jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Settings \u2500\u2500"}),D&&jsx(Text,{dimColor:true,children:"Updating access..."}),!D&&jsx(pn,{mode:b,password:A,onModeChange:S=>{S==="open"&&x("open");},onPasswordChange:f,onClose:()=>C("main-menu")})]}):jsx(Box,{})}function qo(e){let t=Object.values(e).map(i=>parseFloat(i)).filter(i=>!isNaN(i)).sort((i,c)=>i-c);if(t.length===0)return {average:"-",median:"-",agreement:"-"};let o=t.reduce((i,c)=>i+c,0)/t.length,s=Math.floor(t.length/2),r=t.length%2===0?(t[s-1]+t[s])/2:t[s],l=Object.values(e),a=new Map;for(let i of l)a.set(i,(a.get(i)??0)+1);let u=Math.max(...a.values()),d=l.length>0?Math.round(u/l.length*100):0;return {average:o.toFixed(1),median:r.toFixed(1),agreement:`${d}%`}}function wn(e,t){let n=Math.floor((t-e)/1e3),o=Math.floor(n/60),s=n%60;return `${o}:${s.toString().padStart(2,"0")}`}function Go(e,t){let n=new Map;for(let l of Object.values(e.votes))n.set(l,(n.get(l)??0)+1);let o=[...n.entries()].sort((l,a)=>a[1]-l[1]||l[0].localeCompare(a[0])).map(([l,a])=>`${l}\xD7${a}`).join(", "),s=e.title?` ${e.title}`:"",r=e.timerStartedAt!==void 0&&e.revealedAt!==void 0?` \u2014 ${wn(e.timerStartedAt,e.revealedAt)}`:"";return `Round ${t}:${s} \u2014 ${o||"(no votes)"}${r}`}function yn({projection:e,onlineIds:t,isHost:n,participantId:o,rounds:s,currentRound:r,settingsOpen:l,presetOverrides:a,customScales:u,onStartRound:d,onNewRound:i,onRevote:c,onOpenSettings:g,onCloseSettings:T,onChangeName:E,onChangeScale:C,onChangeDisplayName:O,onChangeAccess:U,onHandoffHost:p,displayName:m,accessMode:b,confirmingQuit:y,roundTitlePromptActive:A,roundTitleInput:f,onRoundTitleChange:D,onRoundTitleSubmit:v,onRoundTitleCancel:k}){let[B,Y]=nt.useState("idle"),[_,j]=nt.useState(null);useEffect(()=>{n||(Y("idle"),j(null));},[n]),useInput(w=>{n&&((w==="e"||w==="E")&&g(),(w==="s"||w==="S")&&d(),(w==="h"||w==="H")&&Y("picking"),r?.revealedAt!==void 0&&((w==="n"||w==="N")&&i(),(w==="v"||w==="V")&&c()));},{isActive:!l&&!y&&B==="idle"&&!A});let h=e.participants.filter(w=>w.id!==o&&t.has(w.id));useInput(w=>{if(B==="picking"){let Z=parseInt(w,10);!isNaN(Z)&&Z>=1&&Z<=h.length?(j(h[Z-1].id),Y("confirming")):(w==="Escape"||w==="q"||w==="Q")&&Y("idle");}},{isActive:B==="picking"}),useInput(w=>{B==="confirming"&&_&&(w==="y"||w==="Y"?(p(_),Y("idle"),j(null)):(w==="n"||w==="N"||w==="Escape")&&(Y("idle"),j(null)));},{isActive:B==="confirming"});let x=s.length>0,S=s.filter(w=>w.revealedAt!==void 0),N=r!==null?S.slice(0,-1):S;return l?jsx(hn,{sessionName:e.settings.name,displayName:m,currentScale:e.settings.defaultScale,accessMode:b,presetOverrides:a,customScales:u,onChangeName:E,onChangeScale:C,onChangeDisplayName:O,onChangeAccess:U,onClose:T}):jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:x?"Session":"Waiting for participants..."}),!x&&jsx(Text,{dimColor:true,children:"No rounds started yet."}),jsx(ze,{participants:e.participants,onlineIds:t,hostId:e.settings.hostParticipantId,votes:{},revealed:false}),r?.revealedAt!==void 0&&jsx(Qo,{projection:e,round:r,rounds:s}),N.length>0&&jsxs(Box,{flexDirection:"column",children:[jsx(Text,{dimColor:true,children:"\u2500\u2500 History \u2500\u2500"}),N.map(w=>{let Z=s.indexOf(w)+1;return jsx(Text,{dimColor:true,children:Go(w,Z)},w.id)})]}),B==="picking"&&h.length>0&&jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{children:"Hand off to:"}),h.map((w,Z)=>jsxs(Text,{children:["[",Z+1,"] ",w.displayName]},w.id)),jsx(Text,{dimColor:true,children:"[q] Cancel"})]}),B==="picking"&&h.length===0&&jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{dimColor:true,children:"No other online participants"}),jsx(Text,{dimColor:true,children:"[q] Cancel"})]}),B==="confirming"&&_&&jsx(Box,{flexDirection:"column",children:jsxs(Text,{children:["Hand off to ",e.participants.find(w=>w.id===_)?.displayName,"? [y/n]"]})}),A&&jsxs(Box,{flexDirection:"column",children:[jsxs(Box,{gap:1,children:[jsx(Text,{children:"Round title (optional, Enter to skip):"}),jsx(W,{value:f,onChange:D,onSubmit:v,onCancel:k,placeholder:"Enter to skip",isActive:A})]}),jsx(Text,{dimColor:true,children:"[Esc] Cancel"})]}),B==="idle"&&!A&&jsxs(Box,{gap:2,children:[n?jsx(Fragment,{children:r?.revealedAt!==void 0?jsxs(Fragment,{children:[jsx(Text,{color:"yellow",children:"[v] Re-vote"}),jsx(Text,{color:"yellow",children:"[n] Next round"}),jsx(Text,{color:"yellow",children:"[h] Hand off"}),jsx(Text,{color:"yellow",children:"[e] Settings"})]}):jsxs(Fragment,{children:[jsx(Text,{color:"yellow",children:"[s] Start round"}),jsx(Text,{color:"yellow",children:"[h] Hand off"}),jsx(Text,{color:"yellow",children:"[e] Settings"})]})}):jsx(Text,{dimColor:true,children:"Waiting for host to start a round..."}),jsx(Text,{children:"[q] Quit"})]})]})}function Qo({projection:e,round:t,rounds:n}){let o=n.indexOf(t)+1,s=t.title?` \u2014 ${t.title}`:"",r=qo(t.votes),l=new Map(e.participants.map(d=>[d.id,d.displayName])),a=t.timerStartedAt!==void 0&&t.revealedAt!==void 0?wn(t.timerStartedAt,t.revealedAt):null,u=Object.values(t.votes)[0];return jsxs(Box,{flexDirection:"column",gap:1,children:[jsxs(Text,{bold:true,children:["Round ",o,s," \u2014 Results"]}),Yt(t)&&jsxs(Text,{bold:true,color:"green",children:["Consensus! Everyone voted ",u]}),jsxs(Box,{flexDirection:"column",children:[jsxs(Box,{gap:2,children:[jsx(Text,{bold:true,underline:true,children:"Name".padEnd(20)}),jsx(Text,{bold:true,underline:true,children:"Estimate".padEnd(10)})]}),Object.entries(t.votes).map(([d,i])=>jsxs(Box,{gap:2,children:[jsx(Text,{children:(l.get(d)??d).padEnd(20)}),jsx(Text,{color:"cyan",bold:true,children:i.padEnd(10)})]},d)),e.participants.filter(d=>!(d.id in t.votes)).map(d=>jsxs(Box,{gap:2,children:[jsx(Text,{dimColor:true,children:d.displayName.padEnd(20)}),jsx(Text,{dimColor:true,children:"(no vote)".padEnd(10)})]},d.id))]}),jsxs(Box,{gap:2,children:[jsxs(Text,{children:["Avg: ",jsx(Text,{bold:true,children:r.average})]}),jsxs(Text,{children:["Med: ",jsx(Text,{bold:true,children:r.median})]}),jsxs(Text,{children:["Agreement: ",jsx(Text,{bold:true,children:r.agreement})]}),a!==null&&jsxs(Text,{dimColor:true,children:["\u23F1 ",a]})]})]})}function Cn({scale:e,selected:t,onSelect:n,onClear:o,disabled:s}){let[r,l]=useState(0);return useInput((a,u)=>{if(!s)if(u.leftArrow)l(d=>Math.max(0,d-1));else if(u.rightArrow)l(d=>Math.min(e.cards.length-1,d+1));else if(u.return){let d=e.cards[r];d===t?o():n(d);}else (u.backspace||u.delete)&&o();}),jsxs(Box,{flexDirection:"column",gap:0,children:[jsx(Box,{gap:1,children:e.cards.map((a,u)=>{let d=u===r,i=a===t;return jsx(Text,{inverse:d,bold:i,color:i?"cyan":void 0,children:i?`[${a}]`:` ${a} `},a)})}),jsx(Text,{dimColor:true,children:" \u2190\u2192 navigate \u23CE select \u232B clear"})]})}function es(e){let t=Math.floor(e/1e3),n=Math.floor(t/60),o=t%60;return `${n}:${o.toString().padStart(2,"0")}`}function bn({projection:e,round:t,onlineIds:n,isHost:o,myId:s,myRole:r,elapsedMs:l,onVote:a,onClearVote:u,onReveal:d,onToggleRole:i,confirmingQuit:c}){useInput(C=>{o&&(C==="r"||C==="R")&&d(),(C==="w"||C==="W")&&i();},{isActive:!c});let g=t.votes[s],T=e.rounds.indexOf(t)+1,E=t.title?` \u2014 ${t.title}`:"";return jsxs(Box,{flexDirection:"column",gap:1,children:[jsxs(Box,{gap:2,children:[jsxs(Text,{bold:true,children:["Round ",T,E]}),l!==void 0&&jsxs(Text,{dimColor:true,children:["\u23F1 ",es(l)]})]}),jsxs(Box,{gap:4,children:[jsx(ze,{participants:e.participants,onlineIds:n,hostId:e.settings.hostParticipantId,votes:t.votes,revealed:false}),r==="viewer"?jsx(Box,{flexDirection:"column",children:jsx(Text,{dimColor:true,children:"You're observing"})}):jsx(Cn,{scale:t.scale,selected:g,onSelect:a,onClear:u,disabled:c})]}),jsxs(Box,{gap:2,children:[o&&jsx(Text,{color:"yellow",children:"[r] Reveal"}),jsxs(Text,{color:"yellow",children:["[w] ",r==="viewer"?"Vote":"Observe"]}),jsx(Text,{children:"[q] Quit"})]})]})}function os(e){let t=z(e);return !t||t.revealedAt!==void 0?"lobby":"voting"}function tt({session:e,participantId:t,displayName:n,accessMode:o,presetOverrides:s,customScales:r}){let{exit:l}=useApp(),[a,u]=useState(e.projection()),[d,i]=useState("connecting"),[c,g]=useState(e.onlineIds()),[T,E]=useState(void 0),[C,O]=useState(false),[U,p]=useState(o==="password"?"password":"open"),[m,b]=useState(n),[y,A]=useState(false),[f,D]=useState(false),[v,k]=useState(""),[B,Y]=useState(null),_=nt.useRef(null);useEffect(()=>{let P=e.onChange(()=>{u(e.projection()),g(e.onlineIds());}),ee=e.onStatusChange(i),Wn=e.onHostChange(Be=>{_.current&&clearTimeout(_.current);let Gn=e.projection().participants.find(zn=>zn.id===Be),Qn=Be===t?"You are now the host":`${Gn?.displayName||"Unknown"} is now the host`;Y(Qn),_.current=setTimeout(()=>{Y(null),_.current=null;},3e3);}),qn=setInterval(()=>{g(e.onlineIds());let Be=e.projection(),He=z(Be);He?.timerStartedAt!==void 0&&He.revealedAt===void 0?E(Date.now()-He.timerStartedAt):E(void 0);},1e3);return ()=>{P(),ee(),Wn(),clearInterval(qn),_.current&&clearTimeout(_.current);}},[e,t]),useInput(P=>{(P==="q"||P==="Q")&&A(true);},{isActive:!C&&!y&&!f}),useInput((P,ee)=>{P==="y"||P==="Y"?(e.disconnect(),l()):(P==="n"||P==="N"||ee.escape)&&A(false);},{isActive:y});let j=os(a);useEffect(()=>{j!=="lobby"&&O(false);},[j]);let h=z(a),x=a.settings.hostParticipantId===t,S=a.participants.find(P=>P.id===t),N=ie(S??{}),w=useCallback(()=>{k(""),D(true);},[]),Z=useCallback(P=>{D(false),k(""),e.newRound(P.trim()||void 0);},[e]),On=useCallback(()=>{D(false),k("");},[]),Dn=useCallback(P=>{e.vote(P);},[e]),Mn=useCallback(()=>{e.unvote();},[e]),Un=useCallback(()=>{e.reveal();},[e]),Bn=useCallback(()=>{k(""),D(true);},[]),Hn=useCallback(()=>{e.revote();},[e]),jn=useCallback(()=>{e.toggleRole();},[e]),Ln=useCallback(P=>{e.updateName(P);},[e]),kn=useCallback(P=>{e.updateScale(P);},[e]),_n=useCallback(P=>{e.updateDisplayName(P),b(P);},[e]),$n=useCallback(async(P,ee)=>{await e.updateAccess(P,ee),p(P);},[e]),Yn=useCallback(P=>{try{e.handoffHost(P);}catch(ee){console.error(`Handoff failed: ${ee instanceof Error?ee.message:String(ee)}`);}},[e]),Vn=useCallback(()=>{O(true);},[]),Fn=useCallback(()=>{O(false);},[]),st=a.rounds.filter(P=>P.revealedAt!==void 0),Jn=st.length>0?st[st.length-1]:null;return jsxs(Box,{flexDirection:"column",children:[jsx(rn,{roomCode:e.roomCode,sessionName:a.settings.name,status:d,isHost:x}),B&&jsx(Box,{paddingX:1,children:jsx(Text,{color:"yellow",children:B})}),j==="lobby"&&jsx(yn,{projection:a,onlineIds:c,isHost:x,participantId:t,rounds:a.rounds,currentRound:Jn,settingsOpen:C,presetOverrides:s,customScales:r,displayName:m,accessMode:U,onStartRound:w,onNewRound:Bn,roundTitlePromptActive:f,roundTitleInput:v,onRoundTitleChange:k,onRoundTitleSubmit:Z,onRoundTitleCancel:On,onRevote:Hn,onOpenSettings:Vn,onCloseSettings:Fn,onChangeName:Ln,onChangeScale:kn,onChangeDisplayName:_n,onChangeAccess:$n,onHandoffHost:Yn,confirmingQuit:y}),j==="voting"&&h&&jsx(bn,{projection:a,round:h,onlineIds:c,isHost:x,myId:t,myRole:N,elapsedMs:T,onVote:Dn,onClearVote:Mn,onReveal:Un,onToggleRole:jn,confirmingQuit:y}),y&&jsx(Box,{children:jsx(Text,{dimColor:true,children:"Leave session? y/n"})})]})}function En({presetOverrides:e,customScales:t}){let{exit:n}=useApp(),[o,s]=useState("main-menu"),[r,l]=useState(re()),[a,u]=useState(r.displayName??""),[d,i]=useState(r.relayUrl??""),[c,g]=useState(r.defaultSessionName??""),[T,E]=useState(t??[]),[C,O]=useState(e??{});useInput(h=>{(h==="q"||h==="Q")&&n();},{isActive:o==="main-menu"});let U=useMemo(()=>Ve(C,T),[C,T]),p=r.defaultScale,m=p?U.find(h=>h.id===p):void 0,b=[{label:"Display name",value:"display-name",detail:`[${r.displayName??"(none)"}]`},{label:"Relay URL",value:"relay-url",detail:`[${r.relayUrl??"(none)"}]`},{label:"Default scale",value:"default-scale",detail:`[${m?ae(m.id):"(none)"}]`},{label:"Default session name",value:"default-session-name",detail:`[${r.defaultSessionName??"(none)"}]`},{label:"Default password",value:"default-password",detail:`[${r.defaultPassword?"on":"off"}]`}];function y(h){h==="display-name"?(u(r.displayName??""),s("editing-display-name")):h==="relay-url"?(i(r.relayUrl??""),s("editing-relay-url")):h==="default-scale"?s("picking-scale"):h==="default-session-name"?(g(r.defaultSessionName??""),s("editing-session-name")):h==="default-password"&&s("picking-default-password");}function A(h){let x=h.trim();if(x.length>0){let S={...r,displayName:x};l(S),V({displayName:x});}s("main-menu");}function f(h){let x=h.trim();if(x.length>0){let S={...r,relayUrl:x};l(S),V({relayUrl:x});}s("main-menu");}function D(h){let x=h.trim(),S=x.length>0?{...r,defaultSessionName:x}:{...r};x.length===0?(delete S.defaultSessionName,l(S),Ie({defaultSessionName:""})):(l(S),V({defaultSessionName:x})),s("main-menu");}function v(h){let x=h.length>0?{...r,defaultScale:h}:{...r};h.length===0?(delete x.defaultScale,l(x),Ie({defaultScale:""})):(l(x),V({defaultScale:h})),s("main-menu");}function k(h,x){let S={...C};x.length===0?delete S[h]:S[h]=x,O(S),V({presetOverrides:Object.keys(S).length>0?S:void 0});}function B(h){let x=h==="on",S={...r,defaultPassword:x};l(S),V({defaultPassword:x}),s("main-menu");}function Y(h,x){let S=`custom-${randomUUID()}`,N=[...T,{id:S,label:h,cards:x}];E(N),l({...r,customScales:N}),V({customScales:N});}function _(h,x,S){let N=T.map(w=>w.id===h?{id:h,label:x,cards:S}:w);E(N),l({...r,customScales:N}),V({customScales:N});}function j(h){let x=T.filter(N=>N.id!==h);E(x);let S={...r,customScales:x};r.defaultScale===h?(S.defaultScale="fibonacci",V({customScales:x,defaultScale:"fibonacci"})):V({customScales:x}),l(S);}if(o==="main-menu")return jsx(Ne,{title:"\u2500\u2500 Config Settings \u2500\u2500",items:b,onSelect:y,onClose:()=>n()});if(o==="editing-display-name")return jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Config Settings \u2500\u2500"}),jsx(Text,{children:"Display name: "}),jsx(W,{value:a,onChange:u,onSubmit:A,onCancel:()=>s("main-menu"),placeholder:"(required)",isActive:true}),jsx(Text,{dimColor:true,children:"\u23CE confirm esc back"})]});if(o==="editing-relay-url")return jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Config Settings \u2500\u2500"}),jsx(Text,{children:"Relay URL: "}),jsx(W,{value:d,onChange:i,onSubmit:f,onCancel:()=>s("main-menu"),placeholder:"(required)",isActive:true}),jsx(Text,{dimColor:true,children:"\u23CE confirm esc back"})]});if(o==="editing-session-name")return jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Config Settings \u2500\u2500"}),jsx(Text,{children:"Default session name: "}),jsx(W,{value:c,onChange:g,onSubmit:D,onCancel:()=>s("main-menu"),placeholder:"(leave empty to clear)",isActive:true}),jsx(Text,{dimColor:true,children:"\u23CE confirm esc back"})]});if(o==="picking-scale"){let h=[{id:"",cards:[]},...U];return jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Config Settings \u2500\u2500"}),jsx(Ze,{scales:h,selectedScaleId:p??"",presetOverrides:C,customScales:T,onSelect:v,onOverrideChange:k,onCustomScaleCreate:Y,onCustomScaleEdit:_,onCustomScaleDelete:j,onClose:()=>s("main-menu"),showNoneOption:true})]})}if(o==="picking-default-password"){let h=[{label:"On",value:"on",detail:r.defaultPassword?"[current]":void 0},{label:"Off",value:"off",detail:r.defaultPassword?void 0:"[current]"}];return jsxs(Box,{flexDirection:"column",gap:1,children:[jsx(Text,{bold:true,children:"\u2500\u2500 Config Settings \u2500\u2500"}),jsx(Ne,{title:"Default password:",items:h,onSelect:B,onClose:()=>s("main-menu")})]})}return jsx(Box,{})}async function Pt(){return new Promise(e=>{let t=createInterface({input:process.stdin,output:process.stderr});process.stderr.write("Password: "),t.question("",n=>{t.close(),e(n);});})}async function ps(e){let t=await Re(),n=e.scale??t.defaultScale??"fibonacci",o=e.name??t.defaultSessionName,s=e.password??t.defaultPassword??false,r;s&&(r=await Pt());let l=Ye(n,t.presetOverrides,t.customScales);if(!l){let c=Object.keys(Q).join(", ");console.error(`Unknown or fully-disabled scale: "${n}". Builtin IDs: ${c}`),process.exit(1);}let a=await Xt({participantId:t.participantId,displayName:t.displayName,relayUrl:t.relayUrl,sessionName:o,password:r,defaultScale:l}),u;r&&(u=await ce(r,a.roomUuid)),Je({roomCode:a.roomCode,roomUuid:a.roomUuid,passwordHash:u,lastSeenAt:Date.now()}),console.error(`Room created: ${a.roomCode}`),console.error(`Share: hnch join ${a.roomCode}`),console.error("");let{waitUntilExit:d,unmount:i}=render(nt.createElement(tt,{session:a,participantId:t.participantId,displayName:t.displayName,accessMode:r?"password":"open",presetOverrides:t.presetOverrides,customScales:t.customScales}));qe({doc:a.doc,localParticipantId:t.participantId,onEnded:async c=>{console.error(c.isLocal?"Session ended.":`Session ended by ${c.hostName}.`),i();try{await L();}catch(g){console.error(`Failed to clear cached session: ${g instanceof Error?g.message:String(g)}`);}a.disconnect(),process.exit(0);}}),await d(),a.disconnect(),L(),process.exit(0);}async function ms(e){let t=await Re(),n=en(e.code);n===null&&(console.error("invalid room code"),process.exit(1));let o;e.password&&(o=await Pt());let s=await Ee(t.relayUrl,n),r;s.accessMode==="password"&&!e.password?r=await Pt():e.password&&(r=o);let l,a=false,u=await Zt({participantId:t.participantId,displayName:t.displayName,relayUrl:t.relayUrl,roomCode:n,password:r,onRejection(c){if(c==="unknown-room"){We({clearLastSession:L,logger:console,exit:process.exit});return}c==="room-full"&&(a=true,l?.());}}),d;r&&(d=await ce(r,u.roomUuid)),Je({roomCode:u.roomCode,roomUuid:u.roomUuid,passwordHash:d,lastSeenAt:Date.now()}),console.error(`Joined room: ${u.roomCode}`),console.error("");let i=render(nt.createElement(tt,{session:u,participantId:t.participantId,displayName:t.displayName,accessMode:s.accessMode,presetOverrides:t.presetOverrides,customScales:t.customScales}));l=i.unmount,qe({doc:u.doc,localParticipantId:t.participantId,onEnded:async c=>{console.error(c.isLocal?"Session ended.":`Session ended by ${c.hostName}.`),l?.();try{await L();}catch(g){console.error(`Failed to clear cached session: ${g instanceof Error?g.message:String(g)}`);}u.disconnect(),process.exit(0);}}),await i.waitUntilExit(),a&&(console.error("This room is full (max 20 participants)."),u.disconnect(),L(),process.exit(1)),u.disconnect(),L(),process.exit(0);}async function fs(){let e=await Re(),t=nn();t||(console.error("No recent session to resume."),process.exit(0));let n;try{n=await Ee(e.relayUrl,t.roomCode);}catch(i){if((i instanceof Error?i.message:String(i)).includes("Room not found"))return await We({clearLastSession:L,logger:console,exit:process.exit});console.error("Could not reach relay \u2014 try again later."),process.exit(0);}n.accessMode==="password"&&!t.passwordHash&&(console.error(`Password changed \u2014 rejoin with: hnch join ${t.roomCode} --password`),L(),process.exit(0)),console.error(`Rejoining room ${t.roomCode}\u2026`),console.error("");let o=new he.Doc,s=new Awareness(o);$e(o,{participantId:e.participantId,displayName:e.displayName}),s.setLocalStateField("participantId",e.participantId);let r=false,l=false,a,u=Fe({doc:o,awareness:s,roomCode:t.roomCode,roomUuid:t.roomUuid,participantId:e.participantId,relayUrl:e.relayUrl,passwordHash:t.passwordHash,onRejection(i){if(i==="unknown-room"){We({clearLastSession:L,logger:console,exit:process.exit});return}i==="wrong-password"&&(r=true,a?.()),i==="room-full"&&(l=true,a?.());}});Je({roomCode:u.roomCode,roomUuid:u.roomUuid,passwordHash:t.passwordHash,lastSeenAt:Date.now()});let d=render(nt.createElement(tt,{session:u,participantId:e.participantId,displayName:e.displayName,accessMode:n.accessMode,presetOverrides:e.presetOverrides,customScales:e.customScales}));a=d.unmount,qe({doc:u.doc,localParticipantId:e.participantId,onEnded:async i=>{console.error(i.isLocal?"Session ended.":`Session ended by ${i.hostName}.`),a?.();try{await L();}catch(c){console.error(`Failed to clear cached session: ${c instanceof Error?c.message:String(c)}`);}u.disconnect(),process.exit(0);}}),await d.waitUntilExit(),l&&(console.error("This room is full (max 20 participants)."),L(),process.exit(1)),r&&(console.error(`Password changed \u2014 rejoin with: hnch join ${t.roomCode} --password`),L(),process.exit(0)),u.disconnect(),L(),process.exit(0);}async function gs(e){if(!(e.name!==void 0||e.relayUrl!==void 0||e.scale!==void 0||e.sessionName!==void 0||e.password!==void 0)){let o=await Re(),{waitUntilExit:s}=render(nt.createElement(En,{presetOverrides:o.presetOverrides,customScales:o.customScales}));await s(),process.exit(0);}let n={};if(e.name!==void 0&&(n.displayName=e.name),e.relayUrl!==void 0&&(n.relayUrl=e.relayUrl),e.scale!==void 0){if(e.scale!==""){let o=re();if(!Ye(e.scale,o.presetOverrides,o.customScales)){console.error(`Unknown scale: "${e.scale}". Builtin IDs: ${Object.keys(Q).join(", ")}`);return}}n.defaultScale=e.scale;}e.sessionName!==void 0&&(n.defaultSessionName=e.sessionName),e.password!==void 0&&(n.defaultPassword=e.password),Ie(n),console.log("Config updated.");}var An=ds(hideBin(process.argv)).scriptName("hnch").usage("$0 <command> \u2014 terminal pointing poker").command("create","Create a new room",e=>e.option("name",{type:"string",describe:"Session name"}).option("password",{type:"boolean",describe:"Protect room with a password"}).option("scale",{type:"string",describe:"Estimation scale ID (fibonacci, t-shirt, powers-of-2, linear, risk, or custom-<id>)"}),e=>{ps(e).catch(t=>{console.error(t instanceof Error?t.message:t),process.exit(1);});}).command("join <code>","Join an existing room",e=>e.positional("code",{type:"string",demandOption:true,describe:"6-character room code"}).option("password",{type:"boolean",describe:"Room requires a password",default:false}),e=>{ms(e).catch(t=>{console.error(t instanceof Error?t.message:t),process.exit(1);});}).command("resume","Rejoin your last session",e=>e,()=>{fs().catch(e=>{console.error(e instanceof Error?e.message:e),process.exit(1);});}).command("config","View or update CLI config",e=>e.option("name",{type:"string",describe:"Set display name"}).option("relay-url",{type:"string",describe:"Set relay URL"}).option("scale",{type:"string",describe:"Set default estimation scale ID (empty string clears)"}).option("session-name",{type:"string",describe:"Set default session name (empty string clears)"}).option("password",{type:"boolean",describe:"Set default password prompt on (--password) or off (--no-password)"}),e=>{gs(e).catch(t=>{console.error(t instanceof Error?t.message:t),process.exit(1);});}).demandCommand(1,"Specify a command: create, join, resume, or config").strict().help(),Nt=hideBin(process.argv);(Nt.length===0||Nt.length===1&&Nt[0]==="--")&&(An.showHelp(),process.exit(0));An.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xauyxau/hnch",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Pointing poker CLI for agile teams",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pointing-poker",
|
|
7
|
+
"planning-poker",
|
|
8
|
+
"agile",
|
|
9
|
+
"cli",
|
|
10
|
+
"estimation"
|
|
11
|
+
],
|
|
12
|
+
"license": "UNLICENSED",
|
|
13
|
+
"author": "xau",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/IIxauII/hnch.git",
|
|
17
|
+
"directory": "packages/cli"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/IIxauII/hnch#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/IIxauII/hnch/issues"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE.md"
|
|
34
|
+
],
|
|
35
|
+
"type": "module",
|
|
36
|
+
"main": "./dist/index.js",
|
|
37
|
+
"bin": {
|
|
38
|
+
"hnch": "./dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"prepublishOnly": "pnpm run build",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"dev": "tsx src/index.ts",
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"ink": "^5.2.0",
|
|
49
|
+
"react": "^18.3.1",
|
|
50
|
+
"ws": "^8.20.0",
|
|
51
|
+
"y-protocols": "^1.0.7",
|
|
52
|
+
"y-websocket": "^3.0.0",
|
|
53
|
+
"yargs": "^17.7.2",
|
|
54
|
+
"yjs": "^13.6.30"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@hnch/shared": "workspace:*",
|
|
58
|
+
"@types/node": "^22.0.0",
|
|
59
|
+
"@types/react": "^18.3.0",
|
|
60
|
+
"@types/ws": "^8.18.1",
|
|
61
|
+
"@types/yargs": "^17.0.33",
|
|
62
|
+
"tsup": "^8.3.0",
|
|
63
|
+
"tsx": "^4.19.0",
|
|
64
|
+
"typescript": "^5.5.0",
|
|
65
|
+
"vitest": "^3.1.1"
|
|
66
|
+
}
|
|
67
|
+
}
|