hlidskjalf 0.3.6 → 0.3.7
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/CHANGELOG.md +54 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,60 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.7]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Renders on the alternate screen** — the dashboard now runs on the terminal's
|
|
13
|
+
alternate screen buffer (the same one vim/htop/lazygit use), so its frames can
|
|
14
|
+
never accumulate in the scrollback: re-renders repaint in place, and on quit the
|
|
15
|
+
original screen and scrollback are restored exactly as they were before launch.
|
|
16
|
+
This removes the duplicated-header copies that piled up in the scrollback when a
|
|
17
|
+
frame outgrew the viewport.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **URL column shows the full address** — the column now reserves the width its
|
|
22
|
+
longest URL needs before the name column claims any space, so a ready URL is
|
|
23
|
+
shown in full and a long workspace name truncates instead of squeezing the URL
|
|
24
|
+
off-screen. On a terminal too narrow for both, the URL is the one that shrinks
|
|
25
|
+
(and is hidden once nothing's left), while the clickable target stays the
|
|
26
|
+
complete address.
|
|
27
|
+
- **Resize reflows on settle** — a window drag fires a burst of `resize` events;
|
|
28
|
+
the dashboard now waits for the size to settle (~120 ms) before reflowing, so it
|
|
29
|
+
repaints once against the final size rather than thrashing through intermediate
|
|
30
|
+
widths where a row could briefly wrap and malform the frame.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **URL column truncated even with room to spare** — the dashboard's outer
|
|
35
|
+
`overflow: hidden` made Ink slice every line through a tokenizer that miscounts
|
|
36
|
+
OSC 8 hyperlinks (counting the hidden link target as visible width), cutting the
|
|
37
|
+
visible URL to a stray fragment regardless of how much space the column had. The
|
|
38
|
+
clip is gone (the alternate screen makes it unnecessary), so URLs render in full.
|
|
39
|
+
|
|
40
|
+
### Internal
|
|
41
|
+
|
|
42
|
+
- **Alternate-screen lifecycle extracted** — the enter/restore escape sequences
|
|
43
|
+
and the abrupt-exit safety net live in a small `src/terminal.ts` helper, kept
|
|
44
|
+
pure so the behavior is unit-tested without a live TTY.
|
|
45
|
+
|
|
46
|
+
## [0.3.6]
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **Broken URL links and a terminal bell on every repaint** — the URL column's
|
|
51
|
+
OSC 8 hyperlinks were terminated with ST (`ESC \`), but Ink's renderer only
|
|
52
|
+
recognizes the BEL (`\x07`) form: it mis-tokenized the links, dropped the
|
|
53
|
+
visible label on narrow columns, and stranded the bell byte it emits outside a
|
|
54
|
+
valid escape, ringing the terminal on every re-render (e.g. each time the
|
|
55
|
+
selection moved). Links are now BEL-terminated so they round-trip intact and
|
|
56
|
+
silently.
|
|
57
|
+
- **Terminal chime when selecting a process** — a bare control byte such as BEL
|
|
58
|
+
(`\x07`) in a process's own log output survived display sanitization and rang
|
|
59
|
+
the terminal bell when that process's log panel rendered. Sanitization now
|
|
60
|
+
strips bare C0/DEL control bytes (keeping tabs and SGR colour codes).
|
|
61
|
+
|
|
8
62
|
## [0.3.5]
|
|
9
63
|
|
|
10
64
|
### Changed
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,6 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
|
12
12
|
import { pathToFileURL } from 'url';
|
|
13
13
|
|
|
14
14
|
function useCursor(length,enabled){let[cursor,setCursor]=useState(0);return useInput((input,key)=>{key.upArrow||input==="k"?setCursor(i=>Math.max(0,i-1)):(key.downArrow||input==="j")&&setCursor(i=>Math.min(length-1,i+1));},{isActive:enabled}),Math.min(cursor,Math.max(0,length-1))}function useControls({processes,loading,stop,stopProcess,restartProcess,clearLogs}){let[showHelp,setShowHelp]=useState(false),cursor=useCursor(processes.length,!loading&&!showHelp);return useInput((input,key)=>{if(input==="q"||key.ctrl&&input==="c"){stop();return}if(input==="?"){setShowHelp(open=>!open);return}if(showHelp){key.escape&&setShowHelp(false);return}let selected=processes[cursor];selected&&(input==="s"&&(selected.status==="stopped"?restartProcess(selected.workspace.name):stopProcess(selected.workspace.name)),input==="r"&&restartProcess(selected.workspace.name),input==="c"&&clearLogs(selected.workspace.name));}),{cursor,showHelp}}function createCoalescer(flush,intervalMs){let timer=null,fire=()=>{timer=null,flush();};return {schedule(){timer===null&&(timer=setTimeout(fire,intervalMs));},cancel(){timer!==null&&(clearTimeout(timer),timer=null);}}}function isRunning(child){return !!child&&child.exitCode===null&&child.signalCode===null}function killTree(child,signal){let{pid}=child;if(pid!==void 0)try{process.kill(-pid,signal);return}catch{}try{child.kill(signal);}catch{}}function escalateKill(child){let timer=setTimeout(()=>{child.exitCode===null&&killTree(child,"SIGKILL");},5e3);return timer.unref(),timer}var Heartbeat=class{constructor(deps){this.deps=deps;}interval=null;start(){this.interval=setInterval(()=>this.tick(),1e4),this.interval.unref();}stop(){this.interval&&(clearInterval(this.interval),this.interval=null);}tick(){let now=Date.now();for(let[name,entry]of this.deps.entries()){let{status}=entry.process,url=entry.process.url;if(status==="idle"&&url){this.probe(url).then(alive=>{alive&&entry.process.status==="idle"&&(entry.lastOutputAt=Date.now(),this.deps.setStatus(name,entry.lastGoodStatus??"ready"));});continue}status!=="watching"&&status!=="ready"||entry.lastOutputAt&&now-entry.lastOutputAt>3e5&&(url?this.probe(url).then(alive=>{alive?entry.lastOutputAt=Date.now():(entry.process.status==="watching"||entry.process.status==="ready")&&this.deps.setStatus(name,"idle");}):this.deps.setStatus(name,"idle"));}}async probe(url){try{return await(await fetch(url,{signal:AbortSignal.timeout(3e3)})).body?.cancel(),!0}catch{return false}}};function appendLog(logs,line){logs.push(line),logs.length>1e3&&logs.splice(0,logs.length-500);}function visibleLogRange(total,height,scroll){let maxScroll=Math.max(0,total-height),clamped=Math.min(Math.max(0,scroll),maxScroll),end=total-clamped;return {start:Math.max(0,end-height),end,maxScroll}}var ENV_ALLOWLIST=new Set(["HOME","USER","LOGNAME","SHELL","PATH","LANG","LC_ALL","LC_CTYPE","TERM","TERM_PROGRAM","COLORTERM","NODE_ENV","NODE_OPTIONS","NODE_PATH","NPM_CONFIG_REGISTRY","PNPM_HOME","COREPACK_HOME","XDG_CONFIG_HOME","XDG_DATA_HOME","XDG_CACHE_HOME","TMPDIR","TMP","TEMP","EDITOR","DISPLAY","HOSTNAME"]);function safeEnv(source=process.env){let filtered={};for(let key of Object.keys(source))ENV_ALLOWLIST.has(key)&&(filtered[key]=source[key]);return filtered.FORCE_COLOR="1",filtered}function collectDescendants(rootPid,children){let result=[],stack=[rootPid];for(;stack.length>0;){let pid=stack.pop();result.push(pid);let kids=children.get(pid);if(kids)for(let kid of kids)stack.push(kid);}return result}function parseCpuTime(raw){let rest=raw.trim();if(!rest)return 0;let days=0,dash=rest.indexOf("-");if(dash!==-1){if(days=Number.parseInt(rest.slice(0,dash),10),Number.isNaN(days))return 0;rest=rest.slice(dash+1);}let parts=rest.split(":"),seconds=0,multiplier=1;for(let i=parts.length-1;i>=0;i--){let value=Number.parseFloat(parts[i]??"");if(Number.isNaN(value))return 0;seconds+=value*multiplier,multiplier*=60;}return seconds+=days*86400,Math.round(seconds*100)}function parsePsOutput(output){let children=new Map,stats=new Map;for(let line of output.trim().split(`
|
|
15
|
-
`).slice(1)){let parts=line.trim().split(/\s+/);if(parts.length<4)continue;let pid=Number.parseInt(parts[0]??"",10),ppid=Number.parseInt(parts[1]??"",10),cputimeTicks=parseCpuTime(parts[2]??""),rssKb=Number.parseInt(parts[3]??"",10);if(Number.isNaN(pid)||Number.isNaN(ppid))continue;stats.set(pid,{cputimeTicks,rss:(Number.isNaN(rssKb)?0:rssKb)*1024});let kids=children.get(ppid);kids||(kids=[],children.set(ppid,kids)),kids.push(pid);}return {children,stats}}function sumTickDeltas(prev,curr){if(!prev)return 0;let delta=0;for(let[pid,ticks]of curr){let before=prev.get(pid);before!==void 0&&ticks>before&&(delta+=ticks-before);}return delta}function parseProcStat(content,pageSize=4096){let closeParen=content.lastIndexOf(")");if(closeParen===-1)return null;let fields=content.slice(closeParen+2).split(" "),ppid=Number.parseInt(fields[1]??"",10),utime=Number.parseInt(fields[11]??"",10),stime=Number.parseInt(fields[12]??"",10),rss=Number.parseInt(fields[21]??"",10)*pageSize;return Number.isNaN(ppid)?null:{ppid,utime,stime,rss}}function cpuPercentFromTicks(tickDelta,elapsedMs,numCpus,ticksPerSec=100){if(elapsedMs<=0||numCpus<=0)return 0;let elapsedSec=elapsedMs/1e3,cpuPercent=tickDelta/ticksPerSec/elapsedSec/numCpus*100;return Math.max(0,cpuPercent)}var METRICS_INTERVAL_MS=3e3,MIN_METRICS_INTERVAL_MS=1e3,PS_TIMEOUT_MS=5e3,Meter=class{constructor(deps){this.deps=deps;}prevCpuSnapshot=new Map;timer=null;lastSampleAt=0;stopped=false;numCpus=os.availableParallelism();start(){this.collect(),this.schedule(METRICS_INTERVAL_MS);}stop(){this.stopped=true,this.timer&&(clearTimeout(this.timer),this.timer=null);}request(){if(this.stopped)return;let sinceLast=Date.now()-this.lastSampleAt;this.schedule(Math.max(0,MIN_METRICS_INTERVAL_MS-sinceLast));}reset(name){this.prevCpuSnapshot.delete(name);}schedule(delay){this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>{this.timer=null,this.collect(),this.stopped||this.schedule(METRICS_INTERVAL_MS);},delay),this.timer.unref();}collect(){if(this.stopped)return;this.lastSampleAt=Date.now();let roots=this.deps.roots();roots.size!==0&&(process.platform==="linux"?this.collectProc(roots):this.collectPs(roots));}collectProc(roots){let tree=this.readProcTree(),now=Date.now(),changed=false;for(let[rootPid,name]of roots){let pids=collectDescendants(rootPid,tree.children),updated=this.apply(name,pids,now,pid=>{let stat=tree.stats.get(pid);return stat?{ticks:stat.utime+stat.stime,rss:stat.rss}:void 0});changed=changed||updated;}changed&&this.deps.onChange();}collectPs(roots){let output;try{output=execFileSync("ps",["-eo","pid,ppid,time,rss"],{encoding:"utf8",timeout:PS_TIMEOUT_MS});}catch{return}let{children,stats}=parsePsOutput(output),now=Date.now(),changed=false;for(let[rootPid,name]of roots){let pids=collectDescendants(rootPid,children),updated=this.apply(name,pids,now,pid=>{let stat=stats.get(pid);return stat?{ticks:stat.cputimeTicks,rss:stat.rss}:void 0});changed=changed||updated;}changed&&this.deps.onChange();}apply(name,pids,now,statOf){let prev=this.prevCpuSnapshot.get(name),perPid=new Map,totalMem=0;for(let pid of pids){let stat=statOf(pid);stat&&(perPid.set(pid,stat.ticks),totalMem+=stat.rss);}let cpu=prev?cpuPercentFromTicks(sumTickDeltas(prev.perPid,perPid),now-prev.time,this.numCpus):0;return this.prevCpuSnapshot.set(name,{time:now,perPid}),this.deps.setMetrics(name,{cpu,mem:totalMem})}readProcTree(){let children=new Map,stats=new Map,entries;try{entries=fs.readdirSync("/proc");}catch{return {children,stats}}for(let entry of entries){if(!/^\d+$/.test(entry))continue;let pid=Number.parseInt(entry,10);try{let parsed=parseProcStat(fs.readFileSync(`/proc/${pid}/stat`,"utf8"));if(!parsed)continue;let{ppid,utime,stime,rss}=parsed;stats.set(pid,{utime,stime,rss});let kids=children.get(ppid);kids||(kids=[],children.set(ppid,kids)),kids.push(pid);}catch{}}return {children,stats}}};var MAX_PARSE_LENGTH=4096,LOCAL_HOSTS=new Set(["localhost","127.0.0.1","[::1]","0.0.0.0"]);function localOrigin(raw){let cleaned=raw.replace(/[.,;:!?)}\]]+$/,""),parsed;try{parsed=new URL(cleaned);}catch{return}if(!(parsed.protocol!=="http:"&&parsed.protocol!=="https:")&&parsed.port&&LOCAL_HOSTS.has(parsed.hostname))return parsed.origin}var DTS=/\bDTS\b/,baseMatchers=[{pattern:/running on (https?:\/\/\S+)/,status:"ready"},{pattern:/listening on (https?:\/\/\S+)/,status:"ready"},{pattern:/listening at (https?:\/\/\S+)/,status:"ready"},{pattern:/started.*?(https?:\/\/localhost:\d+)/,status:"ready"},{pattern:/\bVITE\b.*?\bready in\b/i,status:"ready"},{pattern:/\bLocal:\s+(https?:\/\/\S+)/,status:"ready"},{pattern:/⚡\uFE0F?\s*Build success/,status:"watching"},{pattern:/Build start/,status:"building"},{pattern:/Watching for changes/,status:"watching"},{pattern:/\[ERROR\]/,status:"error"},{pattern:/error[\s:]/i,status:"error"},{pattern:/process exit/,status:"error"},{pattern:/\blistening\b/i,status:"ready"}],matchers=baseMatchers.map(m=>({...m,needsHttp:m.pattern.source.includes("http")}));function parseLine(line){let truncated=line.length>MAX_PARSE_LENGTH?line.slice(0,MAX_PARSE_LENGTH):line;if(DTS.test(truncated))return {};let hasHttp=truncated.includes("http");for(let{pattern,status,needsHttp}of matchers){if(needsHttp&&!hasHttp)continue;let match=truncated.match(pattern);if(match){let url=match[1]?localOrigin(match[1]):void 0;return {status,url}}}return {}}function stripAnsi(text){return text.includes("\x1B")?stripVTControlCharacters(text):text}function sanitizeForDisplay(text){if(!text.includes("\x1B"))return text;let NON_SGR_ESCAPES=/\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|\[[?>=]*[\d;]*[A-Za-ln-z@~`]|\([A-Za-z]|[^[(\]\x1b])/g;return text.replace(NON_SGR_ESCAPES,"")}var ERROR_RECOVERY_MS=5e3,MAX_RESTART_RETRIES=3,RESTART_DELAY_MS=1e3,STARTUP_TIMEOUT_MS=12e4,MAX_BUFFER_SIZE=65536,MAX_LINE_LENGTH=8192,ProcessRunner=class _ProcessRunner extends EventEmitter{entries=new Map;pendingRebuilds=new Set;heartbeat=null;meter=null;root;stopping=false;allWorkspaces=[];metricsEnabled;constructor(root2,metrics2=false){super(),this.root=root2,this.metricsEnabled=metrics2;}static newEntry(workspace){return {process:{workspace,status:"pending",logs:[]},child:null,errorTimer:null,restartTimer:null,startupTimer:null,lastGoodStatus:null,restartRetries:0,lastOutputAt:0,intentionalExit:false,teardownStarted:false,onClose:null}}get(name){return this.entries.get(name)?.process}async start(workspaces){this.allWorkspaces=workspaces;let packages=workspaces.filter(w=>w.kind==="package"),apps=workspaces.filter(w=>w.kind!=="package");for(let workspace of workspaces)this.entries.set(workspace.name,_ProcessRunner.newEntry(workspace));for(let workspace of packages)this.spawn(workspace);packages.length>0&&await this.waitForPackages(packages.map(p=>p.name));let failedPackages=new Set;for(let pkg of packages){let s=this.entries.get(pkg.name)?.process.status;(s==="error"||s==="stopped"||s==="timeout")&&failedPackages.add(pkg.name);}for(let workspace of apps){let failedDeps=workspace.deps.filter(d=>failedPackages.has(d));if(failedDeps.length>0){let entry=this.entries.get(workspace.name);entry&&(this.note(entry,`warning: dependency ${failedDeps.join(", ")} failed \u2014 starting anyway`),this.emit("change"));}this.spawn(workspace);}this.heartbeat=new Heartbeat({entries:()=>this.entries,setStatus:(name,status)=>this.setStatus(name,status)}),this.heartbeat.start(),this.metricsEnabled&&(this.meter=new Meter({roots:()=>this.runningRoots(),setMetrics:(name,metrics2)=>{let entry=this.entry(name);return entry?(entry.process.metrics=metrics2,true):false},onChange:()=>this.emit("change")}),this.meter.start());}runningRoots(){let roots=new Map;for(let[name,entry]of this.entries)isRunning(entry.child)&&entry.child.pid!==void 0&&roots.set(entry.child.pid,name);return roots}async shutdown(){this.stopping=true,this.heartbeat?.stop(),this.meter?.stop();for(let entry of this.entries.values())entry.errorTimer&&clearTimeout(entry.errorTimer),entry.restartTimer&&clearTimeout(entry.restartTimer),entry.startupTimer&&clearTimeout(entry.startupTimer);for(let child of this.pendingRebuilds)child.kill("SIGTERM");let waiting=[];for(let entry of this.entries.values()){let{child}=entry;isRunning(child)&&waiting.push(new Promise(resolve2=>{let escalate=escalateKill(child);child.on("close",()=>{clearTimeout(escalate),resolve2();}),killTree(child,"SIGTERM");}));}await Promise.all(waiting);}entry(name){return this.entries.get(name)}note(entry,message){appendLog(entry.process.logs,`[hlidskjalf] ${message}`);}clearTimers(entry){entry.restartTimer&&(clearTimeout(entry.restartTimer),entry.restartTimer=null),entry.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null),entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null);}beginTeardown(entry,onClosed){entry.intentionalExit=true;let{child}=entry;if(!isRunning(child)){entry.child=null,onClosed();return}if(entry.onClose=onClosed,!entry.teardownStarted){entry.teardownStarted=true;let escalate=escalateKill(child);child.on("close",()=>{clearTimeout(escalate),entry.child=null,entry.teardownStarted=false;let action=entry.onClose;entry.onClose=null,action?.();});}killTree(child,"SIGTERM");}waitForPackages(names){let remaining=new Set(names);return new Promise(resolve2=>{let check=()=>{for(let name of [...remaining]){let s=this.entry(name)?.process.status;(s==="watching"||s==="error"||s==="stopped"||s==="timeout")&&remaining.delete(name);}remaining.size===0&&(this.off("change",check),resolve2());};this.on("change",check),check();})}spawn(workspace){let child=spawn("pnpm",["--filter",workspace.name,"run","dev"],{cwd:this.root,stdio:"pipe",env:safeEnv(),detached:true}),entry=this.entry(workspace.name);entry&&(entry.child=child,entry.intentionalExit=false),this.setStatus(workspace.name,"building");let startupTimer=setTimeout(()=>{let e=this.entry(workspace.name);e&&(e.startupTimer=null,e.process.status!=="watching"&&e.process.status!=="ready"&&(this.note(e,`startup timeout after ${STARTUP_TIMEOUT_MS/1e3}s`),this.setStatus(workspace.name,"timeout")));},STARTUP_TIMEOUT_MS);startupTimer.unref(),entry&&(entry.startupTimer=startupTimer);let buffer="",onData=data=>{if(buffer+=data.toString(),!buffer.includes(`
|
|
15
|
+
`).slice(1)){let parts=line.trim().split(/\s+/);if(parts.length<4)continue;let pid=Number.parseInt(parts[0]??"",10),ppid=Number.parseInt(parts[1]??"",10),cputimeTicks=parseCpuTime(parts[2]??""),rssKb=Number.parseInt(parts[3]??"",10);if(Number.isNaN(pid)||Number.isNaN(ppid))continue;stats.set(pid,{cputimeTicks,rss:(Number.isNaN(rssKb)?0:rssKb)*1024});let kids=children.get(ppid);kids||(kids=[],children.set(ppid,kids)),kids.push(pid);}return {children,stats}}function sumTickDeltas(prev,curr){if(!prev)return 0;let delta=0;for(let[pid,ticks]of curr){let before=prev.get(pid);before!==void 0&&ticks>before&&(delta+=ticks-before);}return delta}function parseProcStat(content,pageSize=4096){let closeParen=content.lastIndexOf(")");if(closeParen===-1)return null;let fields=content.slice(closeParen+2).split(" "),ppid=Number.parseInt(fields[1]??"",10),utime=Number.parseInt(fields[11]??"",10),stime=Number.parseInt(fields[12]??"",10),rss=Number.parseInt(fields[21]??"",10)*pageSize;return Number.isNaN(ppid)?null:{ppid,utime,stime,rss}}function cpuPercentFromTicks(tickDelta,elapsedMs,numCpus,ticksPerSec=100){if(elapsedMs<=0||numCpus<=0)return 0;let elapsedSec=elapsedMs/1e3,cpuPercent=tickDelta/ticksPerSec/elapsedSec/numCpus*100;return Math.max(0,cpuPercent)}var METRICS_INTERVAL_MS=3e3,MIN_METRICS_INTERVAL_MS=1e3,PS_TIMEOUT_MS=5e3,Meter=class{constructor(deps){this.deps=deps;}prevCpuSnapshot=new Map;timer=null;lastSampleAt=0;stopped=false;numCpus=os.availableParallelism();start(){this.collect(),this.schedule(METRICS_INTERVAL_MS);}stop(){this.stopped=true,this.timer&&(clearTimeout(this.timer),this.timer=null);}request(){if(this.stopped)return;let sinceLast=Date.now()-this.lastSampleAt;this.schedule(Math.max(0,MIN_METRICS_INTERVAL_MS-sinceLast));}reset(name){this.prevCpuSnapshot.delete(name);}schedule(delay){this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>{this.timer=null,this.collect(),this.stopped||this.schedule(METRICS_INTERVAL_MS);},delay),this.timer.unref();}collect(){if(this.stopped)return;this.lastSampleAt=Date.now();let roots=this.deps.roots();roots.size!==0&&(process.platform==="linux"?this.collectProc(roots):this.collectPs(roots));}collectProc(roots){let tree=this.readProcTree(),now=Date.now(),changed=false;for(let[rootPid,name]of roots){let pids=collectDescendants(rootPid,tree.children),updated=this.apply(name,pids,now,pid=>{let stat=tree.stats.get(pid);return stat?{ticks:stat.utime+stat.stime,rss:stat.rss}:void 0});changed=changed||updated;}changed&&this.deps.onChange();}collectPs(roots){let output;try{output=execFileSync("ps",["-eo","pid,ppid,time,rss"],{encoding:"utf8",timeout:PS_TIMEOUT_MS});}catch{return}let{children,stats}=parsePsOutput(output),now=Date.now(),changed=false;for(let[rootPid,name]of roots){let pids=collectDescendants(rootPid,children),updated=this.apply(name,pids,now,pid=>{let stat=stats.get(pid);return stat?{ticks:stat.cputimeTicks,rss:stat.rss}:void 0});changed=changed||updated;}changed&&this.deps.onChange();}apply(name,pids,now,statOf){let prev=this.prevCpuSnapshot.get(name),perPid=new Map,totalMem=0;for(let pid of pids){let stat=statOf(pid);stat&&(perPid.set(pid,stat.ticks),totalMem+=stat.rss);}let cpu=prev?cpuPercentFromTicks(sumTickDeltas(prev.perPid,perPid),now-prev.time,this.numCpus):0;return this.prevCpuSnapshot.set(name,{time:now,perPid}),this.deps.setMetrics(name,{cpu,mem:totalMem})}readProcTree(){let children=new Map,stats=new Map,entries;try{entries=fs.readdirSync("/proc");}catch{return {children,stats}}for(let entry of entries){if(!/^\d+$/.test(entry))continue;let pid=Number.parseInt(entry,10);try{let parsed=parseProcStat(fs.readFileSync(`/proc/${pid}/stat`,"utf8"));if(!parsed)continue;let{ppid,utime,stime,rss}=parsed;stats.set(pid,{utime,stime,rss});let kids=children.get(ppid);kids||(kids=[],children.set(ppid,kids)),kids.push(pid);}catch{}}return {children,stats}}};var MAX_PARSE_LENGTH=4096,LOCAL_HOSTS=new Set(["localhost","127.0.0.1","[::1]","0.0.0.0"]);function localOrigin(raw){let cleaned=raw.replace(/[.,;:!?)}\]]+$/,""),parsed;try{parsed=new URL(cleaned);}catch{return}if(!(parsed.protocol!=="http:"&&parsed.protocol!=="https:")&&parsed.port&&LOCAL_HOSTS.has(parsed.hostname))return parsed.origin}var DTS=/\bDTS\b/,baseMatchers=[{pattern:/running on (https?:\/\/\S+)/,status:"ready"},{pattern:/listening on (https?:\/\/\S+)/,status:"ready"},{pattern:/listening at (https?:\/\/\S+)/,status:"ready"},{pattern:/started.*?(https?:\/\/localhost:\d+)/,status:"ready"},{pattern:/\bVITE\b.*?\bready in\b/i,status:"ready"},{pattern:/\bLocal:\s+(https?:\/\/\S+)/,status:"ready"},{pattern:/⚡\uFE0F?\s*Build success/,status:"watching"},{pattern:/Build start/,status:"building"},{pattern:/Watching for changes/,status:"watching"},{pattern:/\[ERROR\]/,status:"error"},{pattern:/error[\s:]/i,status:"error"},{pattern:/process exit/,status:"error"},{pattern:/\blistening\b/i,status:"ready"}],matchers=baseMatchers.map(m=>({...m,needsHttp:m.pattern.source.includes("http")}));function parseLine(line){let truncated=line.length>MAX_PARSE_LENGTH?line.slice(0,MAX_PARSE_LENGTH):line;if(DTS.test(truncated))return {};let hasHttp=truncated.includes("http");for(let{pattern,status,needsHttp}of matchers){if(needsHttp&&!hasHttp)continue;let match=truncated.match(pattern);if(match){let url=match[1]?localOrigin(match[1]):void 0;return {status,url}}}return {}}function stripAnsi(text){return text.includes("\x1B")?stripVTControlCharacters(text):text}var NON_SGR_ESCAPES=/\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|\[[?>=]*[\d;]*[A-Za-ln-z@~`]|\([A-Za-z]|[^[(\]\x1b])/g,BARE_CONTROLS=/[\x00-\x08\x0b-\x1a\x1c-\x1f\x7f]/g;function sanitizeForDisplay(text){let hasEscape=text.includes("\x1B"),hasControl=text.search(BARE_CONTROLS)!==-1;if(!hasEscape&&!hasControl)return text;let out=text;return hasEscape&&(out=out.replace(NON_SGR_ESCAPES,"")),hasControl&&(out=out.replace(BARE_CONTROLS,"")),out}var ERROR_RECOVERY_MS=5e3,MAX_RESTART_RETRIES=3,RESTART_DELAY_MS=1e3,STARTUP_TIMEOUT_MS=12e4,MAX_BUFFER_SIZE=65536,MAX_LINE_LENGTH=8192,ProcessRunner=class _ProcessRunner extends EventEmitter{entries=new Map;pendingRebuilds=new Set;heartbeat=null;meter=null;root;stopping=false;allWorkspaces=[];metricsEnabled;constructor(root2,metrics2=false){super(),this.root=root2,this.metricsEnabled=metrics2;}static newEntry(workspace){return {process:{workspace,status:"pending",logs:[]},child:null,errorTimer:null,restartTimer:null,startupTimer:null,lastGoodStatus:null,restartRetries:0,lastOutputAt:0,intentionalExit:false,teardownStarted:false,onClose:null}}get(name){return this.entries.get(name)?.process}async start(workspaces){this.allWorkspaces=workspaces;let packages=workspaces.filter(w=>w.kind==="package"),apps=workspaces.filter(w=>w.kind!=="package");for(let workspace of workspaces)this.entries.set(workspace.name,_ProcessRunner.newEntry(workspace));for(let workspace of packages)this.spawn(workspace);packages.length>0&&await this.waitForPackages(packages.map(p=>p.name));let failedPackages=new Set;for(let pkg of packages){let s=this.entries.get(pkg.name)?.process.status;(s==="error"||s==="stopped"||s==="timeout")&&failedPackages.add(pkg.name);}for(let workspace of apps){let failedDeps=workspace.deps.filter(d=>failedPackages.has(d));if(failedDeps.length>0){let entry=this.entries.get(workspace.name);entry&&(this.note(entry,`warning: dependency ${failedDeps.join(", ")} failed \u2014 starting anyway`),this.emit("change"));}this.spawn(workspace);}this.heartbeat=new Heartbeat({entries:()=>this.entries,setStatus:(name,status)=>this.setStatus(name,status)}),this.heartbeat.start(),this.metricsEnabled&&(this.meter=new Meter({roots:()=>this.runningRoots(),setMetrics:(name,metrics2)=>{let entry=this.entry(name);return entry?(entry.process.metrics=metrics2,true):false},onChange:()=>this.emit("change")}),this.meter.start());}runningRoots(){let roots=new Map;for(let[name,entry]of this.entries)isRunning(entry.child)&&entry.child.pid!==void 0&&roots.set(entry.child.pid,name);return roots}async shutdown(){this.stopping=true,this.heartbeat?.stop(),this.meter?.stop();for(let entry of this.entries.values())entry.errorTimer&&clearTimeout(entry.errorTimer),entry.restartTimer&&clearTimeout(entry.restartTimer),entry.startupTimer&&clearTimeout(entry.startupTimer);for(let child of this.pendingRebuilds)child.kill("SIGTERM");let waiting=[];for(let entry of this.entries.values()){let{child}=entry;isRunning(child)&&waiting.push(new Promise(resolve2=>{let escalate=escalateKill(child);child.on("close",()=>{clearTimeout(escalate),resolve2();}),killTree(child,"SIGTERM");}));}await Promise.all(waiting);}entry(name){return this.entries.get(name)}note(entry,message){appendLog(entry.process.logs,`[hlidskjalf] ${message}`);}clearTimers(entry){entry.restartTimer&&(clearTimeout(entry.restartTimer),entry.restartTimer=null),entry.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null),entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null);}beginTeardown(entry,onClosed){entry.intentionalExit=true;let{child}=entry;if(!isRunning(child)){entry.child=null,onClosed();return}if(entry.onClose=onClosed,!entry.teardownStarted){entry.teardownStarted=true;let escalate=escalateKill(child);child.on("close",()=>{clearTimeout(escalate),entry.child=null,entry.teardownStarted=false;let action=entry.onClose;entry.onClose=null,action?.();});}killTree(child,"SIGTERM");}waitForPackages(names){let remaining=new Set(names);return new Promise(resolve2=>{let check=()=>{for(let name of [...remaining]){let s=this.entry(name)?.process.status;(s==="watching"||s==="error"||s==="stopped"||s==="timeout")&&remaining.delete(name);}remaining.size===0&&(this.off("change",check),resolve2());};this.on("change",check),check();})}spawn(workspace){let child=spawn("pnpm",["--filter",workspace.name,"run","dev"],{cwd:this.root,stdio:"pipe",env:safeEnv(),detached:true}),entry=this.entry(workspace.name);entry&&(entry.child=child,entry.intentionalExit=false),this.setStatus(workspace.name,"building");let startupTimer=setTimeout(()=>{let e=this.entry(workspace.name);e&&(e.startupTimer=null,e.process.status!=="watching"&&e.process.status!=="ready"&&(this.note(e,`startup timeout after ${STARTUP_TIMEOUT_MS/1e3}s`),this.setStatus(workspace.name,"timeout")));},STARTUP_TIMEOUT_MS);startupTimer.unref(),entry&&(entry.startupTimer=startupTimer);let buffer="",onData=data=>{if(buffer+=data.toString(),!buffer.includes(`
|
|
16
16
|
`)&&buffer.length>MAX_BUFFER_SIZE){this.handleLine(workspace.name,buffer),buffer="";return}let lines=buffer.split(`
|
|
17
|
-
`);buffer=lines.pop()??"";for(let raw of lines){let line=raw.trimEnd();line&&this.handleLine(workspace.name,line);}};child.stdout?.on("data",onData),child.stderr?.on("data",onData),child.on("close",(code,signal)=>{buffer.trim()&&this.handleLine(workspace.name,buffer.trimEnd()),buffer="",!this.stopping&&(this.entry(workspace.name)?.intentionalExit||this.handleUnexpectedExit(workspace,code,signal));}),child.on("error",()=>{let e=this.entry(workspace.name);e?.startupTimer&&(clearTimeout(e.startupTimer),e.startupTimer=null),this.setStatus(workspace.name,"error");});}handleLine(name,raw){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;let line=raw.length>MAX_LINE_LENGTH?raw.slice(0,MAX_LINE_LENGTH):raw,{process:proc}=entry;appendLog(proc.logs,sanitizeForDisplay(line)),entry.lastOutputAt=Date.now();let prevStatus=proc.status;proc.status==="idle"&&(proc.status=entry.lastGoodStatus??"ready");let{status,url}=parseLine(stripAnsi(line));status&&(status==="error"?this.scheduleErrorRecovery(name):(entry.lastGoodStatus=status,this.clearErrorTimer(name),entry.restartRetries=0,(status==="watching"||status==="ready")&&entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null)),proc.status=status),url&&(proc.url=url),proc.status!==prevStatus&&this.requestMetricsSample(),this.emit("change");}handleUnexpectedExit(workspace,code,signal){if(code===0){this.setStatus(workspace.name,"stopped");return}let entry=this.entry(workspace.name);if(!entry)return;entry.restartRetries+=1;let{restartRetries}=entry;if(restartRetries>MAX_RESTART_RETRIES){this.note(entry,`process exited ${MAX_RESTART_RETRIES} times \u2014 giving up.`),this.setStatus(workspace.name,"error");return}let delay=RESTART_DELAY_MS*2**(restartRetries-1);if(this.note(entry,`process exited unexpectedly (attempt ${restartRetries}/${MAX_RESTART_RETRIES}) \u2014 restarting in ${delay/1e3}s...`),this.setStatus(workspace.name,"error"),signal==="SIGABRT"){this.rebuildFsevents().then(()=>{let e=this.entry(workspace.name);!this.stopping&&e&&!e.intentionalExit&&this.spawn(workspace);}).catch(()=>this.setStatus(workspace.name,"error"));return}let timer=setTimeout(()=>{entry&&(entry.restartTimer=null),this.stopping||this.spawn(workspace);},delay);timer.unref(),entry.restartTimer=timer;}rebuildFsevents(){return new Promise(resolve2=>{let child=spawn("pnpm",["rebuild","fsevents"],{cwd:this.root,stdio:"pipe",env:safeEnv()});this.pendingRebuilds.add(child);let done=()=>{this.pendingRebuilds.delete(child),resolve2();};child.on("close",done),child.on("error",done);})}scheduleErrorRecovery(name){this.clearErrorTimer(name);let entry=this.entry(name);if(!entry)return;let timer=setTimeout(()=>{entry.errorTimer=null,entry.process.status==="error"&&this.setStatus(name,entry.lastGoodStatus??"ready");},ERROR_RECOVERY_MS);timer.unref(),entry.errorTimer=timer;}clearErrorTimer(name){let entry=this.entry(name);entry?.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null);}setStatus(name,status){let entry=this.entry(name);if(!entry)return;let changed=entry.process.status!==status;entry.process.status=status,status==="stopped"&&(entry.process.metrics=void 0),status==="error"&&entry.process.workspace.kind==="package"&&this.notifyDependents(name),changed&&this.requestMetricsSample(),this.emit("change");}stopProcess(name){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;this.clearTimers(entry);let{child}=entry,wasLive=isRunning(child);this.beginTeardown(entry,()=>{entry.restartRetries=0,this.setStatus(name,"stopped");}),wasLive&&(this.note(entry,"stopping process..."),this.emit("change"));}restartProcess(name){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;let workspace=entry.process.workspace,doRestart=()=>{this.stopping||(entry.restartRetries=0,entry.process.url=void 0,this.note(entry,"restarting process..."),this.spawn(workspace));};this.clearTimers(entry);let{child}=entry,wasLive=isRunning(child);this.beginTeardown(entry,doRestart),wasLive&&(this.note(entry,"stopping process for restart..."),this.emit("change"));}clearLogs(name){let entry=this.entry(name);entry&&(entry.process.logs.length=0,this.emit("change"));}addWorkspace(workspace){this.stopping||this.entries.has(workspace.name)||(this.allWorkspaces.push(workspace),this.entries.set(workspace.name,_ProcessRunner.newEntry(workspace)),this.spawn(workspace));}removeWorkspace(name){let entry=this.entry(name);entry&&(this.clearTimers(entry),this.beginTeardown(entry,()=>{}),this.entries.delete(name),this.allWorkspaces=this.allWorkspaces.filter(w=>w.name!==name),this.meter?.reset(name),this.emit("change"));}requestMetricsSample(){this.meter?.request();}notifyDependents(failedName){for(let workspace of this.allWorkspaces){if(!workspace.deps.includes(failedName))continue;let entry=this.entry(workspace.name);entry&&this.note(entry,`warning: dependency ${failedName} entered error state`);}}};function createRunner(root2,metrics2=false){return new ProcessRunner(root2,metrics2)}var WORKSPACE_DIRS=["packages","apps","services"],DEBOUNCE_MS=300;function watchWorkspaces(root2,onChange){let parentWatchers=[],childWatchers=new Map,timer=null,closed=false,schedule=()=>{closed||(timer&&clearTimeout(timer),timer=setTimeout(()=>{timer=null,onChange();},DEBOUNCE_MS),timer.unref());},watchChild=dir=>{if(!(closed||childWatchers.has(dir)))try{let w=watch(dir,(_event,filename)=>{(!filename||filename.toString()==="package.json")&&schedule();});w.on("error",()=>{}),childWatchers.set(dir,w);}catch{}},syncChildren=()=>{if(!closed){for(let dir of WORKSPACE_DIRS){let base=join(root2,dir);try{for(let entry of readdirSync(base,{withFileTypes:!0}))entry.isDirectory()&&watchChild(join(base,entry.name));}catch{}}for(let[dir,w]of childWatchers)existsSync(dir)||(w.close(),childWatchers.delete(dir));}};for(let dir of WORKSPACE_DIRS){let base=join(root2,dir);if(existsSync(base))try{let w=watch(base,()=>{syncChildren(),schedule();});w.on("error",()=>{}),parentWatchers.push(w);}catch{}}return syncChildren(),{close(){closed=true,timer&&clearTimeout(timer);for(let w of parentWatchers)w.close();for(let w of childWatchers.values())w.close();childWatchers.clear();}}}function isPlainObject(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}var VALID_PKG_NAME=/^(@[a-z0-9\-~][a-z0-9\-._~]*\/)?[a-z0-9\-~][a-z0-9\-._~]*$/;function isValidPackageName(name){return VALID_PKG_NAME.test(name)&&name.length<=214}function normalizeFilters(raw){return raw.map(v=>v.replace(/^\{(.+)\}$/,"$1")).filter(v=>{let name=v.endsWith("...")?v.slice(0,-3):v;return isValidPackageName(name)?true:(console.error(`Ignoring invalid filter: ${name}`),false)})}function stringRecord(value){if(!isPlainObject(value))return;let result={};for(let[key,v]of Object.entries(value))typeof v=="string"&&(result[key]=v);return result}function readJson(path){try{let raw=JSON.parse(readFileSync(path,"utf-8"));if(!isPlainObject(raw))return null;let obj=raw,name=typeof obj.name=="string"?obj.name:void 0,scripts=stringRecord(obj.scripts),dependencies=stringRecord(obj.dependencies);return {name,scripts,dependencies}}catch{return null}}function workspaceDeps(pkg){return Object.entries(pkg.dependencies??{}).filter(([name,v])=>v.startsWith("workspace:")&&isValidPackageName(name)).map(([name])=>name)}var kindOrder={package:0,app:1,service:1};function discover(root2){let results=[],dirs=[["packages","package"],["apps","app"],["services","service"]],resolvedRoot=resolve(root2);for(let[dir,kind]of dirs){let base=join(resolvedRoot,dir);if(existsSync(base))for(let entry of readdirSync(base,{withFileTypes:true})){if(!entry.isDirectory())continue;let entryPath=join(base,entry.name);try{if(!realpathSync(entryPath).startsWith(resolvedRoot+sep))continue}catch{continue}let pkg=readJson(join(entryPath,"package.json"));pkg?.name&&isValidPackageName(pkg.name)&&pkg.name!=="hlidskjalf"&&pkg.scripts?.dev&&results.push({name:pkg.name,kind,deps:workspaceDeps(pkg)});}}return results}function sortByDeps(workspaces){let names=new Set(workspaces.map(w=>w.name)),depCount=new Map;for(let workspace of workspaces){let count=0;for(let dep of workspace.deps)names.has(dep)&&count++;depCount.set(workspace,count);}return [...workspaces].sort((a,b)=>a.kind!==b.kind?kindOrder[a.kind]-kindOrder[b.kind]:(depCount.get(a)??0)-(depCount.get(b)??0))}function sortByName(workspaces){return [...workspaces].sort((a,b)=>a.kind!==b.kind?kindOrder[a.kind]-kindOrder[b.kind]:a.name.localeCompare(b.name))}function filterWorkspaces(workspaces,patterns){let byName=new Map(workspaces.map(w=>[w.name,w])),matches=new Set;for(let pattern of patterns){let transitive=pattern.endsWith("..."),name=transitive?pattern.slice(0,-3):pattern;byName.has(name)&&matches.add(name),transitive&&collectDeps(name,byName,matches);}return workspaces.filter(w=>matches.has(w.name))}function collectDeps(name,byName,collected){let workspace=byName.get(name);if(workspace)for(let dep of workspace.deps)byName.has(dep)&&!collected.has(dep)&&(collected.add(dep),collectDeps(dep,byName,collected));}var RENDER_THROTTLE_MS=16;function useRunner(options2){let{exit}=useApp(),[loading,setLoading]=useState(true),[processes,setProcesses]=useState([]),runnerRef=useRef(null),coalescerRef=useRef(null),watcherRef=useRef(null),displayOrderRef=useRef([]),stoppingRef=useRef(false),stop=useCallback(()=>{if(stoppingRef.current)return;stoppingRef.current=true,watcherRef.current?.close();let runner=runnerRef.current;runner?runner.shutdown().catch(()=>{}).finally(()=>exit()):exit();},[exit]);useEffect(()=>{let discoverWorkspaces=()=>{let found=discover(options2.root);return options2.filter?filterWorkspaces(found,options2.filter):found},sortForDisplay=workspaces=>options2.order==="run"?sortByDeps(workspaces):sortByName(workspaces);return (async()=>{let workspaces=discoverWorkspaces();if(workspaces.length===0){console.error("No matching workspaces found."),exit();return}let startOrder=sortByDeps(workspaces),sorted=sortForDisplay(workspaces);displayOrderRef.current=sorted.map(w=>w.name);let runner=createRunner(options2.root,options2.metrics);runnerRef.current=runner,setProcesses(sorted.map(w=>({workspace:w,status:"pending",logs:[]})));let coalescer=createCoalescer(()=>{setProcesses(displayOrderRef.current.flatMap(name=>{let p=runner.get(name);return p?[p]:[]}));},RENDER_THROTTLE_MS);if(coalescerRef.current=coalescer,runner.on("change",coalescer.schedule),options2.watch){let rediscover=()=>{if(stoppingRef.current)return;let fresh=discoverWorkspaces(),freshNames=new Set(fresh.map(w=>w.name)),currentNames=new Set(displayOrderRef.current),added=fresh.filter(w=>!currentNames.has(w.name)),removed=[...currentNames].filter(name=>!freshNames.has(name));if(!(added.length===0&&removed.length===0)){for(let name of removed)runner.removeWorkspace(name);for(let workspace of added)runner.addWorkspace(workspace);displayOrderRef.current=sortForDisplay(fresh).map(w=>w.name),coalescer.schedule();}};watcherRef.current=watchWorkspaces(options2.root,rediscover);}setLoading(false),await runner.start(startOrder);})().catch(err=>{console.error("Fatal:",err instanceof Error?err.message:"unexpected error"),exit();}),process.on("SIGTERM",stop),()=>{process.off("SIGTERM",stop),watcherRef.current?.close(),coalescerRef.current?.cancel();}},[exit,options2.filter,options2.metrics,options2.order,options2.root,options2.watch,stop]);let stopProcess=useCallback(name=>{runnerRef.current?.stopProcess(name);},[]),restartProcess=useCallback(name=>{runnerRef.current?.restartProcess(name);},[]),clearLogs=useCallback(name=>{runnerRef.current?.clearLogs(name);},[]);return {processes,loading,stop,stopProcess,restartProcess,clearLogs}}var ESC="\x1B",HOME_SEQUENCES=new Set([`${ESC}[H`,`${ESC}[1~`,`${ESC}[7~`,`${ESC}OH`]),END_SEQUENCES=new Set([`${ESC}[F`,`${ESC}[4~`,`${ESC}[8~`,`${ESC}OF`]);function useLogScroll(total,height,selectionKey,enabled){let[scroll,setScroll]=useState(0),[prevKey,setPrevKey]=useState(selectionKey);selectionKey!==prevKey&&(setPrevKey(selectionKey),setScroll(0));let[prevTotal,setPrevTotal]=useState(total);if(total!==prevTotal){let delta=total-prevTotal;setPrevTotal(total),scroll>0&&delta>0&&setScroll(s=>s+delta);}let maxScroll=Math.max(0,total-height),maxScrollRef=useRef(maxScroll);maxScrollRef.current=maxScroll,useInput((_input,key)=>{key.pageUp?setScroll(s=>Math.min(Math.min(s,maxScroll)+height,maxScroll)):key.pageDown&&setScroll(s=>Math.max(0,Math.min(s,maxScroll)-height));},{isActive:enabled});let{internal_eventEmitter:emitter}=useStdin();useEffect(()=>{if(!enabled||!emitter)return;let onInput=data=>{HOME_SEQUENCES.has(data)?setScroll(maxScrollRef.current):END_SEQUENCES.has(data)&&setScroll(0);};return emitter.on("input",onInput),()=>{emitter.off("input",onInput);}},[enabled,emitter]);let{start,end}=visibleLogRange(total,height,scroll);return {start,end,atBottom:Math.min(scroll,maxScroll)===0}}var FALLBACK={columns:80,rows:24};function readSize(stdout){return {columns:stdout?.columns??FALLBACK.columns,rows:stdout?.rows??FALLBACK.rows}}function useTerminalSize(){let{stdout}=useStdout(),[size,setSize]=useState(()=>readSize(stdout));return useEffect(()=>{if(!stdout)return;let onResize=()=>{setSize(prev=>{let next=readSize(stdout);return next.columns===prev.columns&&next.rows===prev.rows?prev:next});};return stdout.on("resize",onResize),onResize(),()=>{stdout.off("resize",onResize);}},[stdout]),size}function nameColumnWidth(processes,min=14){let width=min;for(let proc of processes){let candidate=proc.workspace.name.length+2;candidate>width&&(width=candidate);}return width}var COLUMN_WIDTHS={indicator:2,kind:6,status:14,cpu:8,mem:9},ROW_PADDING_X=1,ROW_CHROME_WIDTH=ROW_PADDING_X*2+COLUMN_WIDTHS.indicator+COLUMN_WIDTHS.kind+COLUMN_WIDTHS.status,METRICS_WIDTH=COLUMN_WIDTHS.cpu+COLUMN_WIDTHS.mem;function fitNameColumnWidth(naturalWidth,columns,metrics2){let available=columns-ROW_CHROME_WIDTH-(metrics2?METRICS_WIDTH:0);return Math.max(1,Math.min(naturalWidth,available))}function urlColumnWidth(columns,nameWidth,metrics2){return columns-nameWidth-ROW_CHROME_WIDTH-(metrics2?METRICS_WIDTH:0)}function logPanelHeight(rows,processCount){return Math.max(3,rows-processCount-11)}var OSC8="\x1B]8;;";function hyperlink(url,label=url){return `${OSC8}${url}\x07${label}${OSC8}\x07`}function truncateEnd(text,width){return width<=0?"":text.length<=width?text:width===1?"\u2026":`${text.slice(0,width-1)}\u2026`}var colors={accent:"#7C8EF2",accentBright:"#A3B1FF",success:"#50E3A4",warning:"#F5C542",error:"#F2716B",pending:"#6B7280",highlight:"#5EEAD4",muted:"#6B7280",dim:"#4B5563",separator:"#374151",url:"#93C5FD"},statusDisplay={pending:{color:colors.pending,label:"pending",icon:"\u25CB"},building:{color:colors.warning,label:"building",icon:"\u25D1"},watching:{color:colors.success,label:"watching",icon:"\u25CF"},ready:{color:colors.success,label:"watching",icon:"\u25CF"},error:{color:colors.error,label:"error",icon:"\u2716"},stopped:{color:colors.pending,label:"stopped",icon:"\u25CB"},idle:{color:colors.warning,label:"idle",icon:"\u25D1"},timeout:{color:colors.error,label:"timeout",icon:"\u2716"}};function Header({title:title2,ready=false,columns,hints}){let showHints=hints&&columns>=10+hints.length+4;return jsx(Box,{flexDirection:"column",paddingX:1,paddingTop:1,paddingBottom:1,borderStyle:"single",borderColor:colors.separator,borderTop:false,borderLeft:false,borderRight:false,children:jsxs(Box,{gap:2,children:[jsxs(Box,{flexShrink:0,gap:1,children:[jsx(Text,{color:ready?colors.success:colors.accent,children:"\u25CF"}),jsx(Text,{color:colors.accentBright,bold:true,children:title2})]}),showHints&&jsx(Box,{flexGrow:1,justifyContent:"flex-end",children:jsx(Text,{color:colors.dim,wrap:"truncate-end",children:hints})})]})})}var kindLabel={package:"pkg",app:"app",service:"svc"},HINTS="? help q quit";function StatusGlyph({status,icon}){return status==="building"?jsx(Spinner,{type:"dots"}):jsx(Text,{children:icon})}function formatCpu(cpu){return `${cpu.toFixed(1)}%`.padStart(6)}function formatMem(bytes){let s;return bytes<1024*1024?s=`${(bytes/1024).toFixed(0)} K`:bytes<1024*1024*1024?s=`${(bytes/(1024*1024)).toFixed(1)} M`:s=`${(bytes/(1024*1024*1024)).toFixed(1)} G`,s.padStart(7)}function memColor(bytes){return bytes>512*1024*1024?colors.error:bytes>256*1024*1024?colors.warning:colors.muted}function ProcessRow({process:proc,selected,nameWidth,showMetrics,urlWidth}){let{color,label,icon}=statusDisplay[proc.status];return jsxs(Box,{paddingX:1,children:[jsx(Text,{color:selected?colors.highlight:colors.dim,children:selected?"\u25B8":" "}),jsx(Text,{children:" "}),jsx(Box,{width:nameWidth,children:jsx(Text,{color:selected?colors.highlight:void 0,bold:selected,wrap:"truncate",children:proc.workspace.name})}),jsx(Box,{width:COLUMN_WIDTHS.kind,children:jsx(Text,{color:colors.muted,children:kindLabel[proc.workspace.kind]})}),jsx(Box,{width:COLUMN_WIDTHS.status,children:jsxs(Text,{color,children:[jsx(StatusGlyph,{status:proc.status,icon})," ",label]})}),showMetrics&&jsx(MetricsCells,{metrics:proc.metrics}),proc.url&&urlWidth>0&&jsx(Box,{width:urlWidth,children:jsx(Text,{color:colors.url,wrap:"truncate",children:hyperlink(proc.url,truncateEnd(proc.url,urlWidth))})})]})}function MetricsCells({metrics:metrics2}){return metrics2?jsxs(Fragment,{children:[jsx(Box,{width:COLUMN_WIDTHS.cpu,children:jsx(Text,{color:metrics2.cpu>80?colors.error:colors.muted,children:formatCpu(metrics2.cpu)})}),jsx(Box,{width:COLUMN_WIDTHS.mem,children:jsx(Text,{color:memColor(metrics2.mem),children:formatMem(metrics2.mem)})})]}):jsxs(Fragment,{children:[jsx(Box,{width:COLUMN_WIDTHS.cpu,children:jsx(Text,{color:colors.dim,children:"\u2014"})}),jsx(Box,{width:COLUMN_WIDTHS.mem,children:jsx(Text,{color:colors.dim,children:"\u2014"})})]})}function LogPanel({process:proc,height,start,end,atBottom}){let logLines=proc.logs.slice(start,end),fillCount=height-logLines.length,hidden=proc.logs.length-end;return jsxs(Box,{flexDirection:"column",height:height+3,overflow:"hidden",marginX:1,marginTop:1,borderStyle:"round",borderColor:colors.separator,paddingX:1,children:[jsxs(Box,{marginBottom:1,children:[jsx(Text,{color:colors.accentBright,bold:true,children:"Logs"}),!atBottom&&jsxs(Text,{color:colors.warning,children:[" ","\u23F8 scrolled \xB7 ",hidden," below \xB7 End to follow"]})]}),logLines.map((line,i)=>jsx(Text,{wrap:"truncate",children:line},i)),Array.from({length:fillCount},(_,i)=>jsx(Text,{children:" "},`fill-${i}`))]})}function Dashboard({processes,selectedIndex,title:title2,metrics:metrics2=false}){let{columns:cols,rows}=useTerminalSize(),allReady=useMemo(()=>processes.length>0&&processes.every(p=>p.status==="ready"||p.status==="watching"),[processes]),naturalNameWidth=useMemo(()=>nameColumnWidth(processes),[processes]),nameWidth=fitNameColumnWidth(naturalNameWidth,cols,metrics2),urlWidth=urlColumnWidth(cols,nameWidth,metrics2),logHeight=logPanelHeight(rows,processes.length),safeIndex=Math.min(selectedIndex,Math.max(0,processes.length-1)),selected=processes[safeIndex],scroll=useLogScroll(selected?.logs.length??0,logHeight,selected?.workspace.name??"",!!selected);return jsxs(Box,{flexDirection:"column",height:rows-1,overflow:"hidden",children:[jsx(Header,{title:title2,ready:allReady,columns:cols,hints:HINTS}),jsxs(Box,{paddingX:1,marginLeft:2,marginTop:1,children:[jsx(Box,{width:nameWidth,children:jsx(Text,{color:colors.muted,bold:true,children:"Name"})}),jsx(Box,{width:COLUMN_WIDTHS.kind,children:jsx(Text,{color:colors.muted,bold:true,children:"Kind"})}),jsx(Box,{width:COLUMN_WIDTHS.status,children:jsx(Text,{color:colors.muted,bold:true,children:"Status"})}),metrics2&&jsxs(Fragment,{children:[jsx(Box,{width:COLUMN_WIDTHS.cpu,children:jsx(Text,{color:colors.muted,bold:true,children:"CPU"})}),jsx(Box,{width:COLUMN_WIDTHS.mem,children:jsx(Text,{color:colors.muted,bold:true,children:"MEM"})})]}),jsx(Text,{color:colors.muted,bold:true,children:"URL"})]}),processes.map((proc,i)=>jsx(ProcessRow,{process:proc,selected:i===safeIndex,nameWidth,showMetrics:metrics2,urlWidth},proc.workspace.name)),selected&&jsx(LogPanel,{process:selected,height:logHeight,start:scroll.start,end:scroll.end,atBottom:scroll.atBottom})]})}var BINDINGS=[["\u2191/\u2193","Select process"],["s","Stop / start process"],["r","Restart process"],["c","Clear logs"],["PgUp/PgDn","Scroll logs"],["Home/End","Jump to oldest / newest"],["?","Toggle help"],["q","Quit"]];function Help({title:title2}){let{columns:cols}=useTerminalSize(),keyWidth=Math.max(...BINDINGS.map(([keys])=>keys.length));return jsxs(Box,{flexDirection:"column",children:[jsx(Header,{title:title2,columns:cols,hints:HINTS}),jsxs(Box,{flexDirection:"column",alignSelf:"flex-start",marginX:1,marginTop:1,paddingX:2,paddingY:1,borderStyle:"round",borderColor:colors.separator,children:[jsx(Box,{marginBottom:1,children:jsx(Text,{color:colors.accentBright,bold:true,children:"Keybindings"})}),BINDINGS.map(([keys,action])=>jsxs(Box,{gap:2,children:[jsx(Box,{width:keyWidth,children:jsx(Text,{color:colors.highlight,children:keys})}),jsx(Text,{color:colors.muted,children:action})]},keys)),jsx(Box,{marginTop:1,children:jsx(Text,{color:colors.dim,children:"Press ? or Esc to close"})})]})]})}function Loading({title:title2}){let{columns:cols}=useTerminalSize();return jsxs(Box,{flexDirection:"column",children:[jsx(Header,{title:title2,columns:cols}),jsxs(Box,{marginTop:1,paddingX:2,children:[jsxs(Text,{color:colors.accent,children:[jsx(Spinner,{type:"dots"})," "]}),jsx(Text,{color:colors.muted,children:"Discovering workspaces..."})]})]})}function App({options:options2}){let{processes,loading,stop,stopProcess,restartProcess,clearLogs}=useRunner(options2),{cursor,showHelp}=useControls({processes,loading,stop,stopProcess,restartProcess,clearLogs});return loading?jsx(Loading,{title:options2.title}):showHelp?jsx(Help,{title:options2.title}):jsx(Dashboard,{processes,selectedIndex:cursor,title:options2.title,metrics:options2.metrics})}var CONFIG_FILES=["hlidskjalf.config.ts","hlidskjalf.config.mjs","hlidskjalf.config.js"],PACKAGE_JSON_KEY="hlidskjalf";function validate(raw,source){if(!isPlainObject(raw))return console.error(`Ignoring ${source}: expected a config object.`),{};let obj=raw,config2={};if(Array.isArray(obj.filter)){let strings=obj.filter.filter(v=>typeof v=="string"),filter2=normalizeFilters(strings);filter2.length&&(config2.filter=filter2);}return (obj.order==="run"||obj.order==="alphabetical")&&(config2.order=obj.order),typeof obj.title=="string"&&(config2.title=obj.title),typeof obj.metrics=="boolean"&&(config2.metrics=obj.metrics),typeof obj.watch=="boolean"&&(config2.watch=obj.watch),config2}function fromPackageJson(root2){let path=join(root2,"package.json");if(!existsSync(path))return {};let parsed;try{parsed=JSON.parse(readFileSync(path,"utf-8"));}catch{return {}}if(!isPlainObject(parsed))return {};let key=parsed[PACKAGE_JSON_KEY];return key===void 0?{}:validate(key,`package.json "${PACKAGE_JSON_KEY}" key`)}async function fromConfigFile(root2){for(let name of CONFIG_FILES){let path=join(root2,name);if(existsSync(path))try{let mod=await import(pathToFileURL(path).href);return validate(mod.default??mod,name)}catch(err){return console.error(`Ignoring ${name}: ${err instanceof Error?err.message:"failed to load"}`),{}}}return {}}async function loadConfig(root2){let fromPkg=fromPackageJson(root2),fromFile=await fromConfigFile(root2);return {...fromPkg,...fromFile}}var argv=process.argv.slice(2),explicit={},args=argv.filter(arg=>arg==="--no-metrics"?(explicit.metrics=false,false):arg==="--no-watch"?(explicit.watch=false,false):true),{values}=parseArgs({args,options:{filter:{type:"string",multiple:true},order:{type:"string"},title:{type:"string"},metrics:{type:"boolean"},watch:{type:"boolean"}}}),root=process.cwd(),config=await loadConfig(root),cliFilter=values.filter?normalizeFilters(values.filter):void 0,filter=cliFilter?.length?cliFilter:config.filter,rawOrder=values.order??config.order,order=rawOrder==="run"?"run":"alphabetical",title=values.title??config.title??"Hlidskjalf",metrics=explicit.metrics??values.metrics??config.metrics??false,watch2=explicit.watch??values.watch??config.watch??true,options={root,order,filter:filter?.length?filter:void 0,title,metrics,watch:watch2},{waitUntilExit}=render(jsx(App,{options}),{exitOnCtrlC:false});await waitUntilExit();process.exit(0);
|
|
17
|
+
`);buffer=lines.pop()??"";for(let raw of lines){let line=raw.trimEnd();line&&this.handleLine(workspace.name,line);}};child.stdout?.on("data",onData),child.stderr?.on("data",onData),child.on("close",(code,signal)=>{buffer.trim()&&this.handleLine(workspace.name,buffer.trimEnd()),buffer="",!this.stopping&&(this.entry(workspace.name)?.intentionalExit||this.handleUnexpectedExit(workspace,code,signal));}),child.on("error",()=>{let e=this.entry(workspace.name);e?.startupTimer&&(clearTimeout(e.startupTimer),e.startupTimer=null),this.setStatus(workspace.name,"error");});}handleLine(name,raw){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;let line=raw.length>MAX_LINE_LENGTH?raw.slice(0,MAX_LINE_LENGTH):raw,{process:proc}=entry;appendLog(proc.logs,sanitizeForDisplay(line)),entry.lastOutputAt=Date.now();let prevStatus=proc.status;proc.status==="idle"&&(proc.status=entry.lastGoodStatus??"ready");let{status,url}=parseLine(stripAnsi(line));status&&(status==="error"?this.scheduleErrorRecovery(name):(entry.lastGoodStatus=status,this.clearErrorTimer(name),entry.restartRetries=0,(status==="watching"||status==="ready")&&entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null)),proc.status=status),url&&(proc.url=url),proc.status!==prevStatus&&this.requestMetricsSample(),this.emit("change");}handleUnexpectedExit(workspace,code,signal){if(code===0){this.setStatus(workspace.name,"stopped");return}let entry=this.entry(workspace.name);if(!entry)return;entry.restartRetries+=1;let{restartRetries}=entry;if(restartRetries>MAX_RESTART_RETRIES){this.note(entry,`process exited ${MAX_RESTART_RETRIES} times \u2014 giving up.`),this.setStatus(workspace.name,"error");return}let delay=RESTART_DELAY_MS*2**(restartRetries-1);if(this.note(entry,`process exited unexpectedly (attempt ${restartRetries}/${MAX_RESTART_RETRIES}) \u2014 restarting in ${delay/1e3}s...`),this.setStatus(workspace.name,"error"),signal==="SIGABRT"){this.rebuildFsevents().then(()=>{let e=this.entry(workspace.name);!this.stopping&&e&&!e.intentionalExit&&this.spawn(workspace);}).catch(()=>this.setStatus(workspace.name,"error"));return}let timer=setTimeout(()=>{entry&&(entry.restartTimer=null),this.stopping||this.spawn(workspace);},delay);timer.unref(),entry.restartTimer=timer;}rebuildFsevents(){return new Promise(resolve2=>{let child=spawn("pnpm",["rebuild","fsevents"],{cwd:this.root,stdio:"pipe",env:safeEnv()});this.pendingRebuilds.add(child);let done=()=>{this.pendingRebuilds.delete(child),resolve2();};child.on("close",done),child.on("error",done);})}scheduleErrorRecovery(name){this.clearErrorTimer(name);let entry=this.entry(name);if(!entry)return;let timer=setTimeout(()=>{entry.errorTimer=null,entry.process.status==="error"&&this.setStatus(name,entry.lastGoodStatus??"ready");},ERROR_RECOVERY_MS);timer.unref(),entry.errorTimer=timer;}clearErrorTimer(name){let entry=this.entry(name);entry?.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null);}setStatus(name,status){let entry=this.entry(name);if(!entry)return;let changed=entry.process.status!==status;entry.process.status=status,status==="stopped"&&(entry.process.metrics=void 0),status==="error"&&entry.process.workspace.kind==="package"&&this.notifyDependents(name),changed&&this.requestMetricsSample(),this.emit("change");}stopProcess(name){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;this.clearTimers(entry);let{child}=entry,wasLive=isRunning(child);this.beginTeardown(entry,()=>{entry.restartRetries=0,this.setStatus(name,"stopped");}),wasLive&&(this.note(entry,"stopping process..."),this.emit("change"));}restartProcess(name){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;let workspace=entry.process.workspace,doRestart=()=>{this.stopping||(entry.restartRetries=0,entry.process.url=void 0,this.note(entry,"restarting process..."),this.spawn(workspace));};this.clearTimers(entry);let{child}=entry,wasLive=isRunning(child);this.beginTeardown(entry,doRestart),wasLive&&(this.note(entry,"stopping process for restart..."),this.emit("change"));}clearLogs(name){let entry=this.entry(name);entry&&(entry.process.logs.length=0,this.emit("change"));}addWorkspace(workspace){this.stopping||this.entries.has(workspace.name)||(this.allWorkspaces.push(workspace),this.entries.set(workspace.name,_ProcessRunner.newEntry(workspace)),this.spawn(workspace));}removeWorkspace(name){let entry=this.entry(name);entry&&(this.clearTimers(entry),this.beginTeardown(entry,()=>{}),this.entries.delete(name),this.allWorkspaces=this.allWorkspaces.filter(w=>w.name!==name),this.meter?.reset(name),this.emit("change"));}requestMetricsSample(){this.meter?.request();}notifyDependents(failedName){for(let workspace of this.allWorkspaces){if(!workspace.deps.includes(failedName))continue;let entry=this.entry(workspace.name);entry&&this.note(entry,`warning: dependency ${failedName} entered error state`);}}};function createRunner(root2,metrics2=false){return new ProcessRunner(root2,metrics2)}var WORKSPACE_DIRS=["packages","apps","services"],DEBOUNCE_MS=300;function watchWorkspaces(root2,onChange){let parentWatchers=[],childWatchers=new Map,timer=null,closed=false,schedule=()=>{closed||(timer&&clearTimeout(timer),timer=setTimeout(()=>{timer=null,onChange();},DEBOUNCE_MS),timer.unref());},watchChild=dir=>{if(!(closed||childWatchers.has(dir)))try{let w=watch(dir,(_event,filename)=>{(!filename||filename.toString()==="package.json")&&schedule();});w.on("error",()=>{}),childWatchers.set(dir,w);}catch{}},syncChildren=()=>{if(!closed){for(let dir of WORKSPACE_DIRS){let base=join(root2,dir);try{for(let entry of readdirSync(base,{withFileTypes:!0}))entry.isDirectory()&&watchChild(join(base,entry.name));}catch{}}for(let[dir,w]of childWatchers)existsSync(dir)||(w.close(),childWatchers.delete(dir));}};for(let dir of WORKSPACE_DIRS){let base=join(root2,dir);if(existsSync(base))try{let w=watch(base,()=>{syncChildren(),schedule();});w.on("error",()=>{}),parentWatchers.push(w);}catch{}}return syncChildren(),{close(){closed=true,timer&&clearTimeout(timer);for(let w of parentWatchers)w.close();for(let w of childWatchers.values())w.close();childWatchers.clear();}}}function isPlainObject(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}var VALID_PKG_NAME=/^(@[a-z0-9\-~][a-z0-9\-._~]*\/)?[a-z0-9\-~][a-z0-9\-._~]*$/;function isValidPackageName(name){return VALID_PKG_NAME.test(name)&&name.length<=214}function normalizeFilters(raw){return raw.map(v=>v.replace(/^\{(.+)\}$/,"$1")).filter(v=>{let name=v.endsWith("...")?v.slice(0,-3):v;return isValidPackageName(name)?true:(console.error(`Ignoring invalid filter: ${name}`),false)})}function stringRecord(value){if(!isPlainObject(value))return;let result={};for(let[key,v]of Object.entries(value))typeof v=="string"&&(result[key]=v);return result}function readJson(path){try{let raw=JSON.parse(readFileSync(path,"utf-8"));if(!isPlainObject(raw))return null;let obj=raw,name=typeof obj.name=="string"?obj.name:void 0,scripts=stringRecord(obj.scripts),dependencies=stringRecord(obj.dependencies);return {name,scripts,dependencies}}catch{return null}}function workspaceDeps(pkg){return Object.entries(pkg.dependencies??{}).filter(([name,v])=>v.startsWith("workspace:")&&isValidPackageName(name)).map(([name])=>name)}var kindOrder={package:0,app:1,service:1};function discover(root2){let results=[],dirs=[["packages","package"],["apps","app"],["services","service"]],resolvedRoot=resolve(root2);for(let[dir,kind]of dirs){let base=join(resolvedRoot,dir);if(existsSync(base))for(let entry of readdirSync(base,{withFileTypes:true})){if(!entry.isDirectory())continue;let entryPath=join(base,entry.name);try{if(!realpathSync(entryPath).startsWith(resolvedRoot+sep))continue}catch{continue}let pkg=readJson(join(entryPath,"package.json"));pkg?.name&&isValidPackageName(pkg.name)&&pkg.name!=="hlidskjalf"&&pkg.scripts?.dev&&results.push({name:pkg.name,kind,deps:workspaceDeps(pkg)});}}return results}function sortByDeps(workspaces){let names=new Set(workspaces.map(w=>w.name)),depCount=new Map;for(let workspace of workspaces){let count=0;for(let dep of workspace.deps)names.has(dep)&&count++;depCount.set(workspace,count);}return [...workspaces].sort((a,b)=>a.kind!==b.kind?kindOrder[a.kind]-kindOrder[b.kind]:(depCount.get(a)??0)-(depCount.get(b)??0))}function sortByName(workspaces){return [...workspaces].sort((a,b)=>a.kind!==b.kind?kindOrder[a.kind]-kindOrder[b.kind]:a.name.localeCompare(b.name))}function filterWorkspaces(workspaces,patterns){let byName=new Map(workspaces.map(w=>[w.name,w])),matches=new Set;for(let pattern of patterns){let transitive=pattern.endsWith("..."),name=transitive?pattern.slice(0,-3):pattern;byName.has(name)&&matches.add(name),transitive&&collectDeps(name,byName,matches);}return workspaces.filter(w=>matches.has(w.name))}function collectDeps(name,byName,collected){let workspace=byName.get(name);if(workspace)for(let dep of workspace.deps)byName.has(dep)&&!collected.has(dep)&&(collected.add(dep),collectDeps(dep,byName,collected));}var RENDER_THROTTLE_MS=16;function useRunner(options2){let{exit}=useApp(),[loading,setLoading]=useState(true),[processes,setProcesses]=useState([]),runnerRef=useRef(null),coalescerRef=useRef(null),watcherRef=useRef(null),displayOrderRef=useRef([]),stoppingRef=useRef(false),stop=useCallback(()=>{if(stoppingRef.current)return;stoppingRef.current=true,watcherRef.current?.close();let runner=runnerRef.current;runner?runner.shutdown().catch(()=>{}).finally(()=>exit()):exit();},[exit]);useEffect(()=>{let discoverWorkspaces=()=>{let found=discover(options2.root);return options2.filter?filterWorkspaces(found,options2.filter):found},sortForDisplay=workspaces=>options2.order==="run"?sortByDeps(workspaces):sortByName(workspaces);return (async()=>{let workspaces=discoverWorkspaces();if(workspaces.length===0){console.error("No matching workspaces found."),exit();return}let startOrder=sortByDeps(workspaces),sorted=sortForDisplay(workspaces);displayOrderRef.current=sorted.map(w=>w.name);let runner=createRunner(options2.root,options2.metrics);runnerRef.current=runner,setProcesses(sorted.map(w=>({workspace:w,status:"pending",logs:[]})));let coalescer=createCoalescer(()=>{setProcesses(displayOrderRef.current.flatMap(name=>{let p=runner.get(name);return p?[p]:[]}));},RENDER_THROTTLE_MS);if(coalescerRef.current=coalescer,runner.on("change",coalescer.schedule),options2.watch){let rediscover=()=>{if(stoppingRef.current)return;let fresh=discoverWorkspaces(),freshNames=new Set(fresh.map(w=>w.name)),currentNames=new Set(displayOrderRef.current),added=fresh.filter(w=>!currentNames.has(w.name)),removed=[...currentNames].filter(name=>!freshNames.has(name));if(!(added.length===0&&removed.length===0)){for(let name of removed)runner.removeWorkspace(name);for(let workspace of added)runner.addWorkspace(workspace);displayOrderRef.current=sortForDisplay(fresh).map(w=>w.name),coalescer.schedule();}};watcherRef.current=watchWorkspaces(options2.root,rediscover);}setLoading(false),await runner.start(startOrder);})().catch(err=>{console.error("Fatal:",err instanceof Error?err.message:"unexpected error"),exit();}),process.on("SIGTERM",stop),()=>{process.off("SIGTERM",stop),watcherRef.current?.close(),coalescerRef.current?.cancel();}},[exit,options2.filter,options2.metrics,options2.order,options2.root,options2.watch,stop]);let stopProcess=useCallback(name=>{runnerRef.current?.stopProcess(name);},[]),restartProcess=useCallback(name=>{runnerRef.current?.restartProcess(name);},[]),clearLogs=useCallback(name=>{runnerRef.current?.clearLogs(name);},[]);return {processes,loading,stop,stopProcess,restartProcess,clearLogs}}var ESC="\x1B",HOME_SEQUENCES=new Set([`${ESC}[H`,`${ESC}[1~`,`${ESC}[7~`,`${ESC}OH`]),END_SEQUENCES=new Set([`${ESC}[F`,`${ESC}[4~`,`${ESC}[8~`,`${ESC}OF`]);function useLogScroll(total,height,selectionKey,enabled){let[scroll,setScroll]=useState(0),[prevKey,setPrevKey]=useState(selectionKey);selectionKey!==prevKey&&(setPrevKey(selectionKey),setScroll(0));let[prevTotal,setPrevTotal]=useState(total);if(total!==prevTotal){let delta=total-prevTotal;setPrevTotal(total),scroll>0&&delta>0&&setScroll(s=>s+delta);}let maxScroll=Math.max(0,total-height),maxScrollRef=useRef(maxScroll);maxScrollRef.current=maxScroll,useInput((_input,key)=>{key.pageUp?setScroll(s=>Math.min(Math.min(s,maxScroll)+height,maxScroll)):key.pageDown&&setScroll(s=>Math.max(0,Math.min(s,maxScroll)-height));},{isActive:enabled});let{internal_eventEmitter:emitter}=useStdin();useEffect(()=>{if(!enabled||!emitter)return;let onInput=data=>{HOME_SEQUENCES.has(data)?setScroll(maxScrollRef.current):END_SEQUENCES.has(data)&&setScroll(0);};return emitter.on("input",onInput),()=>{emitter.off("input",onInput);}},[enabled,emitter]);let{start,end}=visibleLogRange(total,height,scroll);return {start,end,atBottom:Math.min(scroll,maxScroll)===0}}var FALLBACK={columns:80,rows:24},RESIZE_SETTLE_MS=120;function readSize(stdout){return {columns:stdout?.columns??FALLBACK.columns,rows:stdout?.rows??FALLBACK.rows}}function useTerminalSize(){let{stdout}=useStdout(),[size,setSize]=useState(()=>readSize(stdout));return useEffect(()=>{if(!stdout)return;let timer,apply=()=>{setSize(prev=>{let next=readSize(stdout);return next.columns===prev.columns&&next.rows===prev.rows?prev:next});},onResize=()=>{timer&&clearTimeout(timer),timer=setTimeout(apply,RESIZE_SETTLE_MS);};return stdout.on("resize",onResize),apply(),()=>{timer&&clearTimeout(timer),stdout.off("resize",onResize);}},[stdout]),size}function nameColumnWidth(processes,min=14){let width=min;for(let proc of processes){let candidate=proc.workspace.name.length+2;candidate>width&&(width=candidate);}return width}function urlContentWidth(processes){let width=0;for(let proc of processes){let length=proc.url?.length??0;length>width&&(width=length);}return width}var COLUMN_WIDTHS={indicator:2,kind:6,status:14,cpu:8,mem:9},ROW_PADDING_X=1,ROW_CHROME_WIDTH=ROW_PADDING_X*2+COLUMN_WIDTHS.indicator+COLUMN_WIDTHS.kind+COLUMN_WIDTHS.status,METRICS_WIDTH=COLUMN_WIDTHS.cpu+COLUMN_WIDTHS.mem;function columnWidths(columns,naturalNameWidth,urlContent,metrics2){let available=columns-ROW_CHROME_WIDTH-(metrics2?METRICS_WIDTH:0);if(available<=1)return {name:Math.max(1,available),url:0};let url=Math.max(0,Math.min(urlContent,available-14));return {name:Math.max(1,Math.min(naturalNameWidth,available-url)),url}}function logPanelHeight(rows,processCount){return Math.max(3,rows-processCount-11)}var OSC8="\x1B]8;;";function hyperlink(url,label=url){return `${OSC8}${url}\x07${label}${OSC8}\x07`}function truncateEnd(text,width){return width<=0?"":text.length<=width?text:width===1?"\u2026":`${text.slice(0,width-1)}\u2026`}var colors={accent:"#7C8EF2",accentBright:"#A3B1FF",success:"#50E3A4",warning:"#F5C542",error:"#F2716B",pending:"#6B7280",highlight:"#5EEAD4",muted:"#6B7280",dim:"#4B5563",separator:"#374151",url:"#93C5FD"},statusDisplay={pending:{color:colors.pending,label:"pending",icon:"\u25CB"},building:{color:colors.warning,label:"building",icon:"\u25D1"},watching:{color:colors.success,label:"watching",icon:"\u25CF"},ready:{color:colors.success,label:"watching",icon:"\u25CF"},error:{color:colors.error,label:"error",icon:"\u2716"},stopped:{color:colors.pending,label:"stopped",icon:"\u25CB"},idle:{color:colors.warning,label:"idle",icon:"\u25D1"},timeout:{color:colors.error,label:"timeout",icon:"\u2716"}};function Header({title:title2,ready=false,columns,hints}){let showHints=hints&&columns>=10+hints.length+4;return jsx(Box,{flexDirection:"column",paddingX:1,paddingTop:1,paddingBottom:1,borderStyle:"single",borderColor:colors.separator,borderTop:false,borderLeft:false,borderRight:false,children:jsxs(Box,{gap:2,children:[jsxs(Box,{flexShrink:0,gap:1,children:[jsx(Text,{color:ready?colors.success:colors.accent,children:"\u25CF"}),jsx(Text,{color:colors.accentBright,bold:true,children:title2})]}),showHints&&jsx(Box,{flexGrow:1,justifyContent:"flex-end",children:jsx(Text,{color:colors.dim,wrap:"truncate-end",children:hints})})]})})}var kindLabel={package:"pkg",app:"app",service:"svc"},HINTS="? help q quit";function StatusGlyph({status,icon}){return status==="building"?jsx(Spinner,{type:"dots"}):jsx(Text,{children:icon})}function formatCpu(cpu){return `${cpu.toFixed(1)}%`.padStart(6)}function formatMem(bytes){let s;return bytes<1024*1024?s=`${(bytes/1024).toFixed(0)} K`:bytes<1024*1024*1024?s=`${(bytes/(1024*1024)).toFixed(1)} M`:s=`${(bytes/(1024*1024*1024)).toFixed(1)} G`,s.padStart(7)}function memColor(bytes){return bytes>512*1024*1024?colors.error:bytes>256*1024*1024?colors.warning:colors.muted}function ProcessRow({process:proc,selected,nameWidth,showMetrics,urlWidth}){let{color,label,icon}=statusDisplay[proc.status];return jsxs(Box,{paddingX:1,children:[jsx(Text,{color:selected?colors.highlight:colors.dim,children:selected?"\u25B8":" "}),jsx(Text,{children:" "}),jsx(Box,{width:nameWidth,children:jsx(Text,{color:selected?colors.highlight:void 0,bold:selected,wrap:"truncate",children:proc.workspace.name})}),jsx(Box,{width:COLUMN_WIDTHS.kind,children:jsx(Text,{color:colors.muted,children:kindLabel[proc.workspace.kind]})}),jsx(Box,{width:COLUMN_WIDTHS.status,children:jsxs(Text,{color,children:[jsx(StatusGlyph,{status:proc.status,icon})," ",label]})}),showMetrics&&jsx(MetricsCells,{metrics:proc.metrics}),proc.url&&urlWidth>0&&jsx(Box,{width:urlWidth,children:jsx(Text,{color:colors.url,wrap:"truncate",children:hyperlink(proc.url,truncateEnd(proc.url,urlWidth))})})]})}function MetricsCells({metrics:metrics2}){return metrics2?jsxs(Fragment,{children:[jsx(Box,{width:COLUMN_WIDTHS.cpu,children:jsx(Text,{color:metrics2.cpu>80?colors.error:colors.muted,children:formatCpu(metrics2.cpu)})}),jsx(Box,{width:COLUMN_WIDTHS.mem,children:jsx(Text,{color:memColor(metrics2.mem),children:formatMem(metrics2.mem)})})]}):jsxs(Fragment,{children:[jsx(Box,{width:COLUMN_WIDTHS.cpu,children:jsx(Text,{color:colors.dim,children:"\u2014"})}),jsx(Box,{width:COLUMN_WIDTHS.mem,children:jsx(Text,{color:colors.dim,children:"\u2014"})})]})}function LogPanel({process:proc,height,start,end,atBottom}){let logLines=proc.logs.slice(start,end),fillCount=height-logLines.length,hidden=proc.logs.length-end;return jsxs(Box,{flexDirection:"column",height:height+3,overflow:"hidden",marginX:1,marginTop:1,borderStyle:"round",borderColor:colors.separator,paddingX:1,children:[jsxs(Box,{marginBottom:1,children:[jsx(Text,{color:colors.accentBright,bold:true,children:"Logs"}),!atBottom&&jsxs(Text,{color:colors.warning,children:[" ","\u23F8 scrolled \xB7 ",hidden," below \xB7 End to follow"]})]}),logLines.map((line,i)=>jsx(Text,{wrap:"truncate",children:line},i)),Array.from({length:fillCount},(_,i)=>jsx(Text,{children:" "},`fill-${i}`))]})}function Dashboard({processes,selectedIndex,title:title2,metrics:metrics2=false}){let{columns:cols,rows}=useTerminalSize(),allReady=useMemo(()=>processes.length>0&&processes.every(p=>p.status==="ready"||p.status==="watching"),[processes]),naturalNameWidth=useMemo(()=>nameColumnWidth(processes),[processes]),urlContent=useMemo(()=>urlContentWidth(processes),[processes]),{name:nameWidth,url:urlWidth}=columnWidths(cols,naturalNameWidth,urlContent,metrics2),logHeight=logPanelHeight(rows,processes.length),safeIndex=Math.min(selectedIndex,Math.max(0,processes.length-1)),selected=processes[safeIndex],scroll=useLogScroll(selected?.logs.length??0,logHeight,selected?.workspace.name??"",!!selected);return jsxs(Box,{flexDirection:"column",height:rows-1,children:[jsx(Header,{title:title2,ready:allReady,columns:cols,hints:HINTS}),jsxs(Box,{paddingX:1,marginLeft:2,marginTop:1,children:[jsx(Box,{width:nameWidth,children:jsx(Text,{color:colors.muted,bold:true,children:"Name"})}),jsx(Box,{width:COLUMN_WIDTHS.kind,children:jsx(Text,{color:colors.muted,bold:true,children:"Kind"})}),jsx(Box,{width:COLUMN_WIDTHS.status,children:jsx(Text,{color:colors.muted,bold:true,children:"Status"})}),metrics2&&jsxs(Fragment,{children:[jsx(Box,{width:COLUMN_WIDTHS.cpu,children:jsx(Text,{color:colors.muted,bold:true,children:"CPU"})}),jsx(Box,{width:COLUMN_WIDTHS.mem,children:jsx(Text,{color:colors.muted,bold:true,children:"MEM"})})]}),jsx(Text,{color:colors.muted,bold:true,children:"URL"})]}),processes.map((proc,i)=>jsx(ProcessRow,{process:proc,selected:i===safeIndex,nameWidth,showMetrics:metrics2,urlWidth},proc.workspace.name)),selected&&jsx(LogPanel,{process:selected,height:logHeight,start:scroll.start,end:scroll.end,atBottom:scroll.atBottom})]})}var BINDINGS=[["\u2191/\u2193","Select process"],["s","Stop / start process"],["r","Restart process"],["c","Clear logs"],["PgUp/PgDn","Scroll logs"],["Home/End","Jump to oldest / newest"],["?","Toggle help"],["q","Quit"]];function Help({title:title2}){let{columns:cols}=useTerminalSize(),keyWidth=Math.max(...BINDINGS.map(([keys])=>keys.length));return jsxs(Box,{flexDirection:"column",children:[jsx(Header,{title:title2,columns:cols,hints:HINTS}),jsxs(Box,{flexDirection:"column",alignSelf:"flex-start",marginX:1,marginTop:1,paddingX:2,paddingY:1,borderStyle:"round",borderColor:colors.separator,children:[jsx(Box,{marginBottom:1,children:jsx(Text,{color:colors.accentBright,bold:true,children:"Keybindings"})}),BINDINGS.map(([keys,action])=>jsxs(Box,{gap:2,children:[jsx(Box,{width:keyWidth,children:jsx(Text,{color:colors.highlight,children:keys})}),jsx(Text,{color:colors.muted,children:action})]},keys)),jsx(Box,{marginTop:1,children:jsx(Text,{color:colors.dim,children:"Press ? or Esc to close"})})]})]})}function Loading({title:title2}){let{columns:cols}=useTerminalSize();return jsxs(Box,{flexDirection:"column",children:[jsx(Header,{title:title2,columns:cols}),jsxs(Box,{marginTop:1,paddingX:2,children:[jsxs(Text,{color:colors.accent,children:[jsx(Spinner,{type:"dots"})," "]}),jsx(Text,{color:colors.muted,children:"Discovering workspaces..."})]})]})}function App({options:options2}){let{processes,loading,stop,stopProcess,restartProcess,clearLogs}=useRunner(options2),{cursor,showHelp}=useControls({processes,loading,stop,stopProcess,restartProcess,clearLogs});return loading?jsx(Loading,{title:options2.title}):showHelp?jsx(Help,{title:options2.title}):jsx(Dashboard,{processes,selectedIndex:cursor,title:options2.title,metrics:options2.metrics})}var CONFIG_FILES=["hlidskjalf.config.ts","hlidskjalf.config.mjs","hlidskjalf.config.js"],PACKAGE_JSON_KEY="hlidskjalf";function validate(raw,source){if(!isPlainObject(raw))return console.error(`Ignoring ${source}: expected a config object.`),{};let obj=raw,config2={};if(Array.isArray(obj.filter)){let strings=obj.filter.filter(v=>typeof v=="string"),filter2=normalizeFilters(strings);filter2.length&&(config2.filter=filter2);}return (obj.order==="run"||obj.order==="alphabetical")&&(config2.order=obj.order),typeof obj.title=="string"&&(config2.title=obj.title),typeof obj.metrics=="boolean"&&(config2.metrics=obj.metrics),typeof obj.watch=="boolean"&&(config2.watch=obj.watch),config2}function fromPackageJson(root2){let path=join(root2,"package.json");if(!existsSync(path))return {};let parsed;try{parsed=JSON.parse(readFileSync(path,"utf-8"));}catch{return {}}if(!isPlainObject(parsed))return {};let key=parsed[PACKAGE_JSON_KEY];return key===void 0?{}:validate(key,`package.json "${PACKAGE_JSON_KEY}" key`)}async function fromConfigFile(root2){for(let name of CONFIG_FILES){let path=join(root2,name);if(existsSync(path))try{let mod=await import(pathToFileURL(path).href);return validate(mod.default??mod,name)}catch(err){return console.error(`Ignoring ${name}: ${err instanceof Error?err.message:"failed to load"}`),{}}}return {}}async function loadConfig(root2){let fromPkg=fromPackageJson(root2),fromFile=await fromConfigFile(root2);return {...fromPkg,...fromFile}}var ENTER_ALT_SCREEN="\x1B[?1049h",EXIT_ALT_SCREEN="\x1B[?1049l";function enterAltScreen(stream=process.stdout){if(!stream.isTTY)return ()=>{};stream.write(ENTER_ALT_SCREEN);let restored=false,restore=()=>{restored||(restored=true,stream.write(EXIT_ALT_SCREEN));};return process.once("exit",restore),restore}var argv=process.argv.slice(2),explicit={},args=argv.filter(arg=>arg==="--no-metrics"?(explicit.metrics=false,false):arg==="--no-watch"?(explicit.watch=false,false):true),{values}=parseArgs({args,options:{filter:{type:"string",multiple:true},order:{type:"string"},title:{type:"string"},metrics:{type:"boolean"},watch:{type:"boolean"}}}),root=process.cwd(),config=await loadConfig(root),cliFilter=values.filter?normalizeFilters(values.filter):void 0,filter=cliFilter?.length?cliFilter:config.filter,rawOrder=values.order??config.order,order=rawOrder==="run"?"run":"alphabetical",title=values.title??config.title??"Hlidskjalf",metrics=explicit.metrics??values.metrics??config.metrics??false,watch2=explicit.watch??values.watch??config.watch??true,options={root,order,filter:filter?.length?filter:void 0,title,metrics,watch:watch2},restoreScreen=enterAltScreen();try{let{waitUntilExit}=render(jsx(App,{options}),{exitOnCtrlC:!1});await waitUntilExit();}finally{restoreScreen();}process.exit(0);
|