hlidskjalf 0.3.2 → 0.3.4
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 +36 -0
- package/README.md +43 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +4 -0
- package/dist/index.js +5 -4
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ 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.4]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Config file** — persist any CLI option in a `hlidskjalf.config.ts` (loaded
|
|
13
|
+
directly via Node's type stripping, no build step) or a `hlidskjalf` key in
|
|
14
|
+
`package.json`, so flags don't have to be retyped. A `defineConfig` helper and
|
|
15
|
+
`Config` type are exported from the package for full type checking. Precedence
|
|
16
|
+
is CLI flags → config file → `package.json` key → defaults.
|
|
17
|
+
- **Watch & re-discover workspaces** — the `packages`, `apps`, and `services`
|
|
18
|
+
directories are watched while running. When a workspace's `package.json` is
|
|
19
|
+
added, removed, or changed, discovery re-runs: new workspaces start
|
|
20
|
+
automatically and removed ones are stopped and dropped from the dashboard. On
|
|
21
|
+
by default; disable with `--no-watch` or `watch: false`.
|
|
22
|
+
|
|
23
|
+
## [0.3.3]
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Help overlay** — press `?` to toggle a full-screen reference listing every
|
|
28
|
+
keybinding. The footer hints collapse to a compact `? help q quit`; press
|
|
29
|
+
`?` again or `Esc` to dismiss, and `q` still quits from anywhere.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Duplicated header** — when the rendered dashboard reached the terminal
|
|
34
|
+
height, Ink fell back to a non-erasing write and stranded the previous frame
|
|
35
|
+
in the scrollback, stacking the header on each redraw (most visible during an
|
|
36
|
+
error/reload burst). The dashboard is now clamped to one line below the
|
|
37
|
+
viewport so every frame stays on the erase path.
|
|
38
|
+
- **Restart killing the UI** — dev processes now run in their own process group,
|
|
39
|
+
so a workspace's teardown signals can no longer terminate hlidskjalf itself.
|
|
40
|
+
Stopping or restarting also signals the whole group, reaping the real server
|
|
41
|
+
under `pnpm` instead of orphaning it (which would keep its port and break the
|
|
42
|
+
next start).
|
|
43
|
+
|
|
8
44
|
## [0.3.2]
|
|
9
45
|
|
|
10
46
|
### Added
|
package/README.md
CHANGED
|
@@ -34,6 +34,49 @@ pnpm dev
|
|
|
34
34
|
| `order` | Sort by `alphabetical` (default) or `run` (`--order=run`) dependency order. |
|
|
35
35
|
| `title` | Custom title for the header (`--title="My App"`). Defaults to `Hlidskjalf`. |
|
|
36
36
|
| `metrics` | Show CPU and memory usage per workspace. Defaults to `false`. |
|
|
37
|
+
| `watch` | Re-discover workspaces when `package.json` files change. Defaults to `true`; disable with `--no-watch`. |
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
Persist any of the options above so they don't have to be retyped on every run.
|
|
42
|
+
Create a `hlidskjalf.config.ts` at the repo root:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { defineConfig } from 'hlidskjalf'
|
|
46
|
+
|
|
47
|
+
export default defineConfig({
|
|
48
|
+
order: 'run',
|
|
49
|
+
metrics: true,
|
|
50
|
+
filter: ['web...'],
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`defineConfig` is optional — a plain `export default { ... }` works too, and
|
|
55
|
+
`hlidskjalf.config.js` / `hlidskjalf.config.mjs` are also recognized. The `.ts`
|
|
56
|
+
form needs no build step: it's loaded directly via Node's type stripping
|
|
57
|
+
(Node ≥ 22.18).
|
|
58
|
+
|
|
59
|
+
Alternatively, add a `hlidskjalf` key to your root `package.json`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"hlidskjalf": {
|
|
64
|
+
"order": "run",
|
|
65
|
+
"metrics": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Precedence is **CLI flags → `hlidskjalf.config.*` → `package.json` key →
|
|
71
|
+
defaults**, so a flag always wins over a stored value.
|
|
72
|
+
|
|
73
|
+
## Watching
|
|
74
|
+
|
|
75
|
+
While running, hlidskjalf watches your `packages`, `apps`, and `services`
|
|
76
|
+
directories. When a workspace's `package.json` is added, removed, or changed it
|
|
77
|
+
re-runs discovery: new workspaces start automatically and removed ones are
|
|
78
|
+
stopped and dropped from the dashboard. Pass `--no-watch` (or set
|
|
79
|
+
`watch: false`) to turn this off.
|
|
37
80
|
|
|
38
81
|
## Controls
|
|
39
82
|
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type SortOrder = 'alphabetical' | 'run';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User-facing configuration. Every field mirrors a CLI flag and is optional;
|
|
5
|
+
* anything omitted falls back to the CLI flag, then to the built-in default.
|
|
6
|
+
* Persist these in a `hlidskjalf.config.ts` file or a `hlidskjalf` key in
|
|
7
|
+
* `package.json` so flags don't have to be retyped on every run.
|
|
8
|
+
*/
|
|
9
|
+
interface Config {
|
|
10
|
+
/** Include only matching workspaces. Append `...` to a name for its transitive deps. */
|
|
11
|
+
filter?: string[];
|
|
12
|
+
/** Sort the dashboard `alphabetical`ly (default) or in dependency `run` order. */
|
|
13
|
+
order?: SortOrder;
|
|
14
|
+
/** Custom header title. Defaults to `Hlidskjalf`. */
|
|
15
|
+
title?: string;
|
|
16
|
+
/** Show CPU and memory usage per workspace. Defaults to `false`. */
|
|
17
|
+
metrics?: boolean;
|
|
18
|
+
/** Re-discover workspaces when `package.json` files change. Defaults to `true`. */
|
|
19
|
+
watch?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Identity helper that gives a `hlidskjalf.config.ts` file full type checking
|
|
23
|
+
* and autocompletion:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { defineConfig } from 'hlidskjalf'
|
|
27
|
+
*
|
|
28
|
+
* export default defineConfig({ order: 'run', metrics: true })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function defineConfig(config: Config): Config;
|
|
32
|
+
|
|
33
|
+
export { type Config, defineConfig };
|
package/dist/config.js
ADDED
package/dist/index.js
CHANGED
|
@@ -4,13 +4,14 @@ import { render, useInput, useApp, useStdout, Box, Text, useStdin } from 'ink';
|
|
|
4
4
|
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
5
5
|
import { spawn, execFileSync } from 'child_process';
|
|
6
6
|
import { EventEmitter } from 'events';
|
|
7
|
-
import fs, { existsSync, readdirSync, realpathSync
|
|
7
|
+
import fs, { existsSync, readFileSync, watch, readdirSync, realpathSync } from 'fs';
|
|
8
8
|
import os from 'os';
|
|
9
|
-
import {
|
|
9
|
+
import { join, resolve, sep } from 'path';
|
|
10
10
|
import Spinner from 'ink-spinner';
|
|
11
11
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
12
13
|
|
|
13
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}),cursor}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 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 parsePsOutput(output){let children=new Map,stats=new Map;for(let line of output.trim().split(`
|
|
14
|
-
`).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),cpu=Number.parseFloat(parts[2]??""),rssKb=Number.parseInt(parts[3]??"",10);if(Number.isNaN(pid)||Number.isNaN(ppid))continue;stats.set(pid,{cpu:Number.isNaN(cpu)?0:cpu,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 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 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:/\blistening\b/i,status:"ready"},{pattern:/\[ERROR\]/,status:"error"},{pattern:/[Ee]rror[\s:]/,status:"error"},{pattern:/process exit/,status:"error"}],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,HEARTBEAT_INTERVAL_MS=1e4,METRICS_INTERVAL_MS=3e3,IDLE_THRESHOLD_MS=3e5,MAX_BUFFER_SIZE=65536,MAX_LINE_LENGTH=8192,ProcessRunner=class extends EventEmitter{entries=new Map;pendingRebuilds=new Set;heartbeatInterval=null;metricsInterval=null;root;stopping=false;allWorkspaces=[];metricsEnabled;prevCpuSnapshot=new Map;numCpus;constructor(
|
|
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),cpu=Number.parseFloat(parts[2]??""),rssKb=Number.parseInt(parts[3]??"",10);if(Number.isNaN(pid)||Number.isNaN(ppid))continue;stats.set(pid,{cpu:Number.isNaN(cpu)?0:cpu,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 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 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:/\blistening\b/i,status:"ready"},{pattern:/\[ERROR\]/,status:"error"},{pattern:/[Ee]rror[\s:]/,status:"error"},{pattern:/process exit/,status:"error"}],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,HEARTBEAT_INTERVAL_MS=1e4,METRICS_INTERVAL_MS=3e3,IDLE_THRESHOLD_MS=3e5,MAX_BUFFER_SIZE=65536,MAX_LINE_LENGTH=8192,ProcessRunner=class _ProcessRunner extends EventEmitter{entries=new Map;pendingRebuilds=new Set;heartbeatInterval=null;metricsInterval=null;root;stopping=false;allWorkspaces=[];metricsEnabled;prevCpuSnapshot=new Map;numCpus;constructor(root2,metrics2=false){super(),this.root=root2,this.metricsEnabled=metrics2,this.numCpus=os.availableParallelism();}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}}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&&(entry.process.logs.push(`[hlidskjalf] warning: dependency ${failedDeps.join(", ")} failed \u2014 starting anyway`),this.emit("change"));}this.spawn(workspace);}this.startHeartbeat(),this.metricsEnabled&&this.startMetrics();}async shutdown(){this.stopping=true,this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.metricsInterval&&clearInterval(this.metricsInterval);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;!child||child.exitCode!==null||child.signalCode!==null||waiting.push(new Promise(resolve2=>{let escalate=setTimeout(()=>{child.exitCode===null&&this.killTree(child,"SIGKILL");},5e3);child.on("close",()=>{clearTimeout(escalate),resolve2();}),this.killTree(child,"SIGTERM");}));}await Promise.all(waiting);}killTree(child,signal){let{pid}=child;if(pid!==void 0)try{process.kill(-pid,signal);return}catch{}try{child.kill(signal);}catch{}}entry(name){return this.entries.get(name)}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"&&(e.process.logs.push(`[hlidskjalf] 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
16
|
`)&&buffer.length>MAX_BUFFER_SIZE){this.handleLine(workspace.name,buffer),buffer="";return}let lines=buffer.split(`
|
|
16
|
-
`);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(),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),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){entry.process.logs.push(`[hlidskjalf] process exited ${MAX_RESTART_RETRIES} times \u2014 giving up.`),this.setStatus(workspace.name,"error");return}let delay=RESTART_DELAY_MS*2**(restartRetries-1);if(entry.process.logs.push(`[hlidskjalf] process exited unexpectedly (attempt ${restartRetries}/${MAX_RESTART_RETRIES}) \u2014 restarting in ${delay/1e3}s...`),this.setStatus(workspace.name,"error"),signal==="SIGABRT"){this.rebuildFsevents().then(()=>{this.stopping||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);})}startHeartbeat(){this.heartbeatInterval=setInterval(()=>{let now=Date.now();for(let[name,entry]of this.entries){let{status}=entry.process,url=entry.process.url;if(status==="idle"&&url){this.probeUrl(url).then(alive=>{alive&&(entry.lastOutputAt=Date.now(),this.setStatus(name,entry.lastGoodStatus??"ready"));});continue}status!=="watching"&&status!=="ready"||entry.lastOutputAt&&now-entry.lastOutputAt>IDLE_THRESHOLD_MS&&(url?this.probeUrl(url).then(alive=>{alive?entry.lastOutputAt=Date.now():this.setStatus(name,"idle");}):this.setStatus(name,"idle"));}},HEARTBEAT_INTERVAL_MS),this.heartbeatInterval.unref();}async probeUrl(url){try{return await(await fetch(url,{signal:AbortSignal.timeout(3e3)})).body?.cancel(),!0}catch{return false}}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);entry&&(entry.process.status=status,status==="error"&&entry.process.workspace.kind==="package"&&this.notifyDependents(name),this.emit("change"));}stopProcess(name){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;entry.restartTimer&&(clearTimeout(entry.restartTimer),entry.restartTimer=null),entry.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null),entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null);let{child}=entry;if(!child||child.exitCode!==null||child.signalCode!==null){this.setStatus(name,"stopped");return}entry.intentionalExit=true;let escalate=setTimeout(()=>{child.exitCode===null&&child.kill("SIGKILL");},5e3);escalate.unref(),child.on("close",()=>{clearTimeout(escalate),entry.child=null,entry.restartRetries=0,this.setStatus(name,"stopped");}),child.kill("SIGTERM"),entry.process.logs.push("[hlidskjalf] 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=()=>{entry.restartRetries=0,entry.process.url=void 0,entry.process.logs.push("[hlidskjalf] restarting process..."),this.spawn(workspace);},{child}=entry;if(!child||child.exitCode!==null||child.signalCode!==null){doRestart();return}entry.intentionalExit=true,entry.restartTimer&&(clearTimeout(entry.restartTimer),entry.restartTimer=null),entry.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null),entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null);let escalate=setTimeout(()=>{child.exitCode===null&&child.kill("SIGKILL");},5e3);escalate.unref(),child.on("close",()=>{clearTimeout(escalate),entry.child=null,doRestart();}),child.kill("SIGTERM"),entry.process.logs.push("[hlidskjalf] stopping process for restart..."),this.emit("change");}clearLogs(name){let entry=this.entry(name);entry&&(entry.process.logs.length=0,this.emit("change"));}startMetrics(){this.collectMetrics(),this.metricsInterval=setInterval(()=>this.collectMetrics(),METRICS_INTERVAL_MS),this.metricsInterval.unref();}collectMetrics(){if(this.stopping)return;let rootPids=new Map;for(let[name,entry]of this.entries){let pid=entry.child?.pid;pid&&entry.child?.exitCode===null&&rootPids.set(pid,name);}rootPids.size!==0&&(process.platform==="linux"?this.collectMetricsProc(rootPids):this.collectMetricsPs(rootPids));}collectMetricsProc(rootPids){let tree=this.readProcTree(),now=Date.now(),changed=false;for(let[rootPid,name]of rootPids){let pids=collectDescendants(rootPid,tree.children),totalTicks=0,totalMem=0;for(let pid of pids){let stat=tree.stats.get(pid);stat&&(totalTicks+=stat.utime+stat.stime,totalMem+=stat.rss);}let prev=this.prevCpuSnapshot.get(name),cpuPercent=0;prev&&(cpuPercent=cpuPercentFromTicks(totalTicks-prev.ticks,now-prev.time,this.numCpus)),this.prevCpuSnapshot.set(name,{ticks:totalTicks,time:now});let entry=this.entry(name);entry&&(entry.process.metrics={cpu:cpuPercent,mem:totalMem},changed=true);}changed&&this.emit("change");}collectMetricsPs(rootPids){let output;try{output=execFileSync("ps",["-eo","pid,ppid,pcpu,rss"],{encoding:"utf8",timeout:5e3});}catch{return}let{children,stats}=parsePsOutput(output),changed=false;for(let[rootPid,name]of rootPids){let pids=collectDescendants(rootPid,children),totalCpu=0,totalMem=0;for(let pid of pids){let stat=stats.get(pid);stat&&(totalCpu+=stat.cpu,totalMem+=stat.rss);}let entry=this.entry(name);entry&&(entry.process.metrics={cpu:totalCpu,mem:totalMem},changed=true);}changed&&this.emit("change");}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}}notifyDependents(failedName){for(let workspace of this.allWorkspaces){if(!workspace.deps.includes(failedName))continue;let entry=this.entry(workspace.name);entry&&entry.process.logs.push(`[hlidskjalf] warning: dependency ${failedName} entered error state`);}}};function createRunner(root,metrics=false){return new ProcessRunner(root,metrics)}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 stringRecord(value){if(typeof value!="object"||value===null||Array.isArray(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(typeof raw!="object"||raw===null||Array.isArray(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(root){let results=[],dirs=[["packages","package"],["apps","app"],["services","service"]],resolvedRoot=resolve(root);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),stoppingRef=useRef(false),stop=useCallback(()=>{if(stoppingRef.current)return;stoppingRef.current=true;let runner=runnerRef.current;runner?runner.shutdown().catch(()=>{}).finally(()=>exit()):exit();},[exit]);useEffect(()=>((async()=>{let workspaces=discover(options2.root);if(options2.filter&&(workspaces=filterWorkspaces(workspaces,options2.filter)),workspaces.length===0){console.error("No matching workspaces found."),exit();return}let startOrder=sortByDeps(workspaces),sorted=options2.order==="run"?startOrder:sortByName(workspaces),displayOrder=sorted.map(w=>w.name),runner=createRunner(options2.root,options2.metrics);runnerRef.current=runner,setProcesses(sorted.map(w=>({workspace:w,status:"pending",logs:[]})));let coalescer=createCoalescer(()=>{setProcesses(displayOrder.flatMap(name=>{let p=runner.get(name);return p?[p]:[]}));},RENDER_THROTTLE_MS);coalescerRef.current=coalescer,runner.on("change",coalescer.schedule),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),coalescerRef.current?.cancel();}),[exit,options2.filter,options2.metrics,options2.order,options2.root,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}}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 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="\u2191/\u2193 select s stop/start r restart c clear PgUp/PgDn scroll 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:6,children:jsx(Text,{color:colors.muted,children:kindLabel[proc.workspace.kind]})}),jsx(Box,{width:14,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:proc.url})})]})}function MetricsCells({metrics}){return metrics?jsxs(Fragment,{children:[jsx(Box,{width:8,children:jsx(Text,{color:metrics.cpu>80?colors.error:colors.muted,children:formatCpu(metrics.cpu)})}),jsx(Box,{width:9,children:jsx(Text,{color:memColor(metrics.mem),children:formatMem(metrics.mem)})})]}):jsxs(Fragment,{children:[jsx(Box,{width:8,children:jsx(Text,{color:colors.dim,children:"\u2014"})}),jsx(Box,{width:9,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"}),jsx(Text,{color:colors.dim,children:" \u203A "}),jsx(Text,{bold:true,children:proc.workspace.name}),!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=false}){let{stdout}=useStdout(),cols=stdout?.columns??80,rows=stdout?.rows??24,allReady=useMemo(()=>processes.length>0&&processes.every(p=>p.status==="ready"||p.status==="watching"),[processes]),nameWidth=useMemo(()=>nameColumnWidth(processes),[processes]),urlWidth=cols-nameWidth-24-(metrics?17:0),logHeight=Math.max(3,rows-processes.length-11),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",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:6,children:jsx(Text,{color:colors.muted,bold:true,children:"Kind"})}),jsx(Box,{width:14,children:jsx(Text,{color:colors.muted,bold:true,children:"Status"})}),metrics&&jsxs(Fragment,{children:[jsx(Box,{width:8,children:jsx(Text,{color:colors.muted,bold:true,children:"CPU"})}),jsx(Box,{width:9,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:metrics,urlWidth},proc.workspace.name)),selected&&jsx(LogPanel,{process:selected,height:logHeight,start:scroll.start,end:scroll.end,atBottom:scroll.atBottom})]})}function Loading({title:title2}){let{stdout}=useStdout(),cols=stdout?.columns??80;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=useCursor(processes.length,!loading);return useInput((input,key)=>{(input==="q"||key.ctrl&&input==="c")&&stop();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));}),loading?jsx(Loading,{title:options2.title}):jsx(Dashboard,{processes,selectedIndex:cursor,title:options2.title,metrics:options2.metrics})}var{values}=parseArgs({args:process.argv.slice(2),options:{filter:{type:"string",multiple:true},order:{type:"string",default:"alphabetical"},title:{type:"string",default:"Hlidskjalf"},metrics:{type:"boolean",default:false}}}),rawFilter=values.filter?.map(v=>v.replace(/^\{(.+)\}$/,"$1")),filter=rawFilter?.filter(v=>{let name=v.endsWith("...")?v.slice(0,-3):v;return isValidPackageName(name)?true:(console.error(`Ignoring invalid filter: ${name}`),false)}),order=values.order==="run"?"run":"alphabetical",title=values.title??"Hlidskjalf",options={root:process.cwd(),order,filter:filter?.length?filter:void 0,title,metrics:values.metrics??false},{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(),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),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){entry.process.logs.push(`[hlidskjalf] process exited ${MAX_RESTART_RETRIES} times \u2014 giving up.`),this.setStatus(workspace.name,"error");return}let delay=RESTART_DELAY_MS*2**(restartRetries-1);if(entry.process.logs.push(`[hlidskjalf] process exited unexpectedly (attempt ${restartRetries}/${MAX_RESTART_RETRIES}) \u2014 restarting in ${delay/1e3}s...`),this.setStatus(workspace.name,"error"),signal==="SIGABRT"){this.rebuildFsevents().then(()=>{this.stopping||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);})}startHeartbeat(){this.heartbeatInterval=setInterval(()=>{let now=Date.now();for(let[name,entry]of this.entries){let{status}=entry.process,url=entry.process.url;if(status==="idle"&&url){this.probeUrl(url).then(alive=>{alive&&(entry.lastOutputAt=Date.now(),this.setStatus(name,entry.lastGoodStatus??"ready"));});continue}status!=="watching"&&status!=="ready"||entry.lastOutputAt&&now-entry.lastOutputAt>IDLE_THRESHOLD_MS&&(url?this.probeUrl(url).then(alive=>{alive?entry.lastOutputAt=Date.now():this.setStatus(name,"idle");}):this.setStatus(name,"idle"));}},HEARTBEAT_INTERVAL_MS),this.heartbeatInterval.unref();}async probeUrl(url){try{return await(await fetch(url,{signal:AbortSignal.timeout(3e3)})).body?.cancel(),!0}catch{return false}}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);entry&&(entry.process.status=status,status==="error"&&entry.process.workspace.kind==="package"&&this.notifyDependents(name),this.emit("change"));}stopProcess(name){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;entry.restartTimer&&(clearTimeout(entry.restartTimer),entry.restartTimer=null),entry.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null),entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null);let{child}=entry;if(!child||child.exitCode!==null||child.signalCode!==null){this.setStatus(name,"stopped");return}entry.intentionalExit=true;let escalate=setTimeout(()=>{child.exitCode===null&&this.killTree(child,"SIGKILL");},5e3);escalate.unref(),child.on("close",()=>{clearTimeout(escalate),entry.child=null,entry.restartRetries=0,this.setStatus(name,"stopped");}),this.killTree(child,"SIGTERM"),entry.process.logs.push("[hlidskjalf] 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=()=>{entry.restartRetries=0,entry.process.url=void 0,entry.process.logs.push("[hlidskjalf] restarting process..."),this.spawn(workspace);},{child}=entry;if(!child||child.exitCode!==null||child.signalCode!==null){doRestart();return}entry.intentionalExit=true,entry.restartTimer&&(clearTimeout(entry.restartTimer),entry.restartTimer=null),entry.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null),entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null);let escalate=setTimeout(()=>{child.exitCode===null&&this.killTree(child,"SIGKILL");},5e3);escalate.unref(),child.on("close",()=>{clearTimeout(escalate),entry.child=null,doRestart();}),this.killTree(child,"SIGTERM"),entry.process.logs.push("[hlidskjalf] 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);if(!entry)return;entry.errorTimer&&clearTimeout(entry.errorTimer),entry.restartTimer&&clearTimeout(entry.restartTimer),entry.startupTimer&&clearTimeout(entry.startupTimer);let{child}=entry;if(child&&child.exitCode===null&&child.signalCode===null){entry.intentionalExit=true;let escalate=setTimeout(()=>{child.exitCode===null&&this.killTree(child,"SIGKILL");},5e3);escalate.unref(),child.on("close",()=>clearTimeout(escalate)),this.killTree(child,"SIGTERM");}this.entries.delete(name),this.allWorkspaces=this.allWorkspaces.filter(w=>w.name!==name),this.prevCpuSnapshot.delete(name),this.emit("change");}startMetrics(){this.collectMetrics(),this.metricsInterval=setInterval(()=>this.collectMetrics(),METRICS_INTERVAL_MS),this.metricsInterval.unref();}collectMetrics(){if(this.stopping)return;let rootPids=new Map;for(let[name,entry]of this.entries){let pid=entry.child?.pid;pid&&entry.child?.exitCode===null&&rootPids.set(pid,name);}rootPids.size!==0&&(process.platform==="linux"?this.collectMetricsProc(rootPids):this.collectMetricsPs(rootPids));}collectMetricsProc(rootPids){let tree=this.readProcTree(),now=Date.now(),changed=false;for(let[rootPid,name]of rootPids){let pids=collectDescendants(rootPid,tree.children),totalTicks=0,totalMem=0;for(let pid of pids){let stat=tree.stats.get(pid);stat&&(totalTicks+=stat.utime+stat.stime,totalMem+=stat.rss);}let prev=this.prevCpuSnapshot.get(name),cpuPercent=0;prev&&(cpuPercent=cpuPercentFromTicks(totalTicks-prev.ticks,now-prev.time,this.numCpus)),this.prevCpuSnapshot.set(name,{ticks:totalTicks,time:now});let entry=this.entry(name);entry&&(entry.process.metrics={cpu:cpuPercent,mem:totalMem},changed=true);}changed&&this.emit("change");}collectMetricsPs(rootPids){let output;try{output=execFileSync("ps",["-eo","pid,ppid,pcpu,rss"],{encoding:"utf8",timeout:5e3});}catch{return}let{children,stats}=parsePsOutput(output),changed=false;for(let[rootPid,name]of rootPids){let pids=collectDescendants(rootPid,children),totalCpu=0,totalMem=0;for(let pid of pids){let stat=stats.get(pid);stat&&(totalCpu+=stat.cpu,totalMem+=stat.rss);}let entry=this.entry(name);entry&&(entry.process.metrics={cpu:totalCpu,mem:totalMem},changed=true);}changed&&this.emit("change");}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}}notifyDependents(failedName){for(let workspace of this.allWorkspaces){if(!workspace.deps.includes(failedName))continue;let entry=this.entry(workspace.name);entry&&entry.process.logs.push(`[hlidskjalf] 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();}}}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(typeof value!="object"||value===null||Array.isArray(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(typeof raw!="object"||raw===null||Array.isArray(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}}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 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:6,children:jsx(Text,{color:colors.muted,children:kindLabel[proc.workspace.kind]})}),jsx(Box,{width:14,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:proc.url})})]})}function MetricsCells({metrics:metrics2}){return metrics2?jsxs(Fragment,{children:[jsx(Box,{width:8,children:jsx(Text,{color:metrics2.cpu>80?colors.error:colors.muted,children:formatCpu(metrics2.cpu)})}),jsx(Box,{width:9,children:jsx(Text,{color:memColor(metrics2.mem),children:formatMem(metrics2.mem)})})]}):jsxs(Fragment,{children:[jsx(Box,{width:8,children:jsx(Text,{color:colors.dim,children:"\u2014"})}),jsx(Box,{width:9,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{stdout}=useStdout(),cols=stdout?.columns??80,rows=stdout?.rows??24,allReady=useMemo(()=>processes.length>0&&processes.every(p=>p.status==="ready"||p.status==="watching"),[processes]),nameWidth=useMemo(()=>nameColumnWidth(processes),[processes]),urlWidth=cols-nameWidth-24-(metrics2?17:0),logHeight=Math.max(3,rows-processes.length-11),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:6,children:jsx(Text,{color:colors.muted,bold:true,children:"Kind"})}),jsx(Box,{width:14,children:jsx(Text,{color:colors.muted,bold:true,children:"Status"})}),metrics2&&jsxs(Fragment,{children:[jsx(Box,{width:8,children:jsx(Text,{color:colors.muted,bold:true,children:"CPU"})}),jsx(Box,{width:9,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{stdout}=useStdout(),cols=stdout?.columns??80,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{stdout}=useStdout(),cols=stdout?.columns??80;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),[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));}),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(typeof raw!="object"||raw===null||Array.isArray(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(typeof parsed!="object"||parsed===null)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??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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hlidskjalf",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "A Terminal User Interface for monitoring Turborepo tasks, built with Ink",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Charlie Beckstrand",
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
"bin": {
|
|
21
21
|
"hlidskjalf": "dist/index.js"
|
|
22
22
|
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/config.d.ts",
|
|
26
|
+
"import": "./dist/config.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
23
29
|
"files": [
|
|
24
30
|
"dist",
|
|
25
31
|
"LICENSE",
|