hlidskjalf 0.3.0 → 0.3.2
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 +202 -0
- package/README.md +27 -0
- package/dist/index.js +5 -6
- package/package.json +9 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.2]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Clear logs** — press `c` to empty the log buffer for the selected workspace.
|
|
13
|
+
- **Scrollable log history** — `PgUp` / `PgDn` page through the retained
|
|
14
|
+
scrollback and `Home` / `End` jump to the oldest / newest lines. The panel
|
|
15
|
+
follows new output until you scroll up, stays anchored to the same lines while
|
|
16
|
+
paused, and snaps back to following when you switch workspaces or clear the
|
|
17
|
+
buffer. A `⏸ scrolled` indicator shows when the view is paused.
|
|
18
|
+
- **Build spinner** — workspaces in the `building` state and the startup screen
|
|
19
|
+
now show an animated spinner in place of the static glyph.
|
|
20
|
+
|
|
21
|
+
## [0.3.1]
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Process runner test coverage** — added tests exercising the process runner.
|
|
26
|
+
- **Hot-path benchmarks** — added [tinybench](https://github.com/tinylibs/tinybench)
|
|
27
|
+
benchmarks for the hot code paths.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **Audit improvements** — hardened workspace discovery, strengthened types, and
|
|
32
|
+
expanded CI coverage.
|
|
33
|
+
- **Built-ins over hand-rolled code** — replaced hand-rolled logic with Node and
|
|
34
|
+
TypeScript built-ins.
|
|
35
|
+
- **Test/benchmark layout** — moved tests and benchmarks into `__tests__` and
|
|
36
|
+
`__benchmarks__` folders.
|
|
37
|
+
|
|
38
|
+
### Performance
|
|
39
|
+
|
|
40
|
+
- Coalesce per-line change events into bounded re-renders.
|
|
41
|
+
- Amortize per-line log-buffer trimming.
|
|
42
|
+
- Skip ANSI stripping on escape-free log lines.
|
|
43
|
+
- Gate URL matchers on a cheap `http` substring check in `parseLine`.
|
|
44
|
+
- Precompute dependency counts in `sortByDeps`.
|
|
45
|
+
- Avoid argument spread in `collectDescendants` and when computing the dashboard
|
|
46
|
+
name-column width.
|
|
47
|
+
|
|
48
|
+
## [0.3.0]
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **Parser bug** — fixed a bug in the log parser.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **Metrics column** — right-align the CPU percentage in the metrics column.
|
|
57
|
+
|
|
58
|
+
## [0.2.8]
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **Metrics column** — right-align the memory unit in the metrics column.
|
|
63
|
+
- **Docs** — document the `--metrics` option in the README.
|
|
64
|
+
|
|
65
|
+
## [0.2.7]
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **Test suite** — added vitest tests for the parser, workspaces, theme, and
|
|
70
|
+
processes modules.
|
|
71
|
+
- **CI** — added a GitHub Actions workflow that runs lint and tests on PRs.
|
|
72
|
+
- **Packaging** — include the `LICENSE` file in the published npm package and
|
|
73
|
+
add a GitHub license badge to the README.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- **URL wrapping** — prevent dashboard URL wrapping from breaking ⌘-click links.
|
|
78
|
+
- **macOS metrics** — fix `--metrics` showing `0`s on macOS by adding a
|
|
79
|
+
`ps`-based fallback.
|
|
80
|
+
|
|
81
|
+
### Removed
|
|
82
|
+
|
|
83
|
+
- Removed the bundled `CHANGELOG.md` file (later reintroduced).
|
|
84
|
+
|
|
85
|
+
## [0.2.6]
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- **Metrics** — use `/proc` for real-time CPU and memory readings.
|
|
90
|
+
|
|
91
|
+
## [0.2.5]
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **Metrics** — aggregate the entire process tree when computing usage.
|
|
96
|
+
- **Header layout** — fix title/shortcut spacing: add a gap, right-align hints,
|
|
97
|
+
and prevent wrapping.
|
|
98
|
+
|
|
99
|
+
## [0.2.4]
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
|
|
103
|
+
- **`--metrics` flag** — show per-process CPU and memory usage.
|
|
104
|
+
- **Workspace controls** — stop and restart individual workspaces.
|
|
105
|
+
- **Licensing** — add an MIT license and a security policy.
|
|
106
|
+
- **Changelog** — add `CHANGELOG.md`.
|
|
107
|
+
|
|
108
|
+
## [0.2.3]
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- **Idle detection** — probe a process's URL before marking it idle so active
|
|
113
|
+
servers stay awake.
|
|
114
|
+
|
|
115
|
+
## [0.2.2]
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
- **Dashboard layout** — fix table column alignment and header spacing, and
|
|
120
|
+
account for header padding in the log-panel height calculation.
|
|
121
|
+
|
|
122
|
+
## [0.1.9]
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
- **Build status** — fix a stuck `building` status when esbuild reports errors.
|
|
127
|
+
|
|
128
|
+
## [0.1.4]
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
|
|
132
|
+
- **`--title` option** — customize the header title.
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- **UI refresh** — modernize the UI with vibrant colors, consistent spacing, and
|
|
137
|
+
proper borders.
|
|
138
|
+
- **Status rename** — rename the `stale` status to `idle`.
|
|
139
|
+
|
|
140
|
+
### Fixed
|
|
141
|
+
|
|
142
|
+
- **URL parsing** — handle trailing punctuation and path suffixes.
|
|
143
|
+
- **Log panel** — stop the log panel height from pushing content beyond the
|
|
144
|
+
terminal viewport.
|
|
145
|
+
|
|
146
|
+
## [0.1.1]
|
|
147
|
+
|
|
148
|
+
### Fixed
|
|
149
|
+
|
|
150
|
+
- **Process health** — fix processes going stale prematurely and not recovering.
|
|
151
|
+
- **Log panel** — constrain the log-panel height to prevent pushing the header
|
|
152
|
+
out of view.
|
|
153
|
+
|
|
154
|
+
## [0.0.8]
|
|
155
|
+
|
|
156
|
+
### Security
|
|
157
|
+
|
|
158
|
+
- Addressed security vulnerabilities across the codebase over several audit
|
|
159
|
+
passes, and switched `sanitizeForDisplay` to a whitelist approach.
|
|
160
|
+
|
|
161
|
+
### Changed
|
|
162
|
+
|
|
163
|
+
- Replaced the custom `stripAnsi` with Node's built-in `stripVTControlCharacters`.
|
|
164
|
+
|
|
165
|
+
## [0.0.5]
|
|
166
|
+
|
|
167
|
+
### Added
|
|
168
|
+
|
|
169
|
+
- **`services/` workspaces** — support `services/` workspace discovery.
|
|
170
|
+
- **Stability** — auto-restart, startup timeout, heartbeat, and graceful
|
|
171
|
+
degradation.
|
|
172
|
+
- **Tooling** — add Biome and lefthook.
|
|
173
|
+
|
|
174
|
+
### Changed
|
|
175
|
+
|
|
176
|
+
- Refactor to clean ES6/React patterns.
|
|
177
|
+
- Reduce bundle size ~30% with syntax and whitespace minification.
|
|
178
|
+
|
|
179
|
+
### Fixed
|
|
180
|
+
|
|
181
|
+
- Fix a shutdown race in `handleLine` and clean up orphaned timers.
|
|
182
|
+
|
|
183
|
+
## [0.0.3]
|
|
184
|
+
|
|
185
|
+
### Changed
|
|
186
|
+
|
|
187
|
+
- Release/packaging bump (no functional changes).
|
|
188
|
+
|
|
189
|
+
## [0.0.2]
|
|
190
|
+
|
|
191
|
+
Initial public release.
|
|
192
|
+
|
|
193
|
+
### Added
|
|
194
|
+
|
|
195
|
+
- Terminal dashboard for visualizing workspace dev servers, with a status-circle
|
|
196
|
+
header.
|
|
197
|
+
|
|
198
|
+
### Changed
|
|
199
|
+
|
|
200
|
+
- Renamed the project from Midgard to Hlidskjalf.
|
|
201
|
+
- Hardened for production: line buffering, shutdown safety, and defensive error
|
|
202
|
+
handling.
|
package/README.md
CHANGED
|
@@ -35,6 +35,33 @@ pnpm dev
|
|
|
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
37
|
|
|
38
|
+
## Controls
|
|
39
|
+
|
|
40
|
+
| Key | Action |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `↑` / `↓` | Move the selection between workspaces |
|
|
43
|
+
| `s` | Stop the selected workspace (or start it again if stopped) |
|
|
44
|
+
| `r` | Restart the selected workspace |
|
|
45
|
+
| `c` | Clear the logs for the selected workspace |
|
|
46
|
+
| `PgUp` / `PgDn` | Scroll the log panel up / down a page |
|
|
47
|
+
| `Home` / `End` | Jump to the oldest / newest log lines (`End` resumes following) |
|
|
48
|
+
| `q` | Quit |
|
|
49
|
+
|
|
50
|
+
## Benchmarks
|
|
51
|
+
|
|
52
|
+
Performance-sensitive code paths — per-log-line parsing, log-buffer appends,
|
|
53
|
+
metrics polling, and workspace ordering — are benchmarked with
|
|
54
|
+
[tinybench](https://github.com/tinylibs/tinybench) under `__benchmarks__/`. Run
|
|
55
|
+
them with:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pnpm bench # all suites
|
|
59
|
+
pnpm bench parser # one or more suites: parser | metrics | workspaces | logs | layout
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each task is warmed up before measuring; the `±` column is the relative margin
|
|
63
|
+
of error, so compare runs only when it stays small.
|
|
64
|
+
|
|
38
65
|
## License
|
|
39
66
|
|
|
40
67
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs, stripVTControlCharacters } from 'util';
|
|
3
|
-
import { render, useInput, useApp, useStdout, Box, Text } from 'ink';
|
|
3
|
+
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
7
|
import fs, { existsSync, readdirSync, realpathSync, readFileSync } from 'fs';
|
|
8
|
-
import http from 'http';
|
|
9
|
-
import https from 'https';
|
|
10
8
|
import os from 'os';
|
|
11
9
|
import { resolve, join, sep } from 'path';
|
|
10
|
+
import Spinner from 'ink-spinner';
|
|
12
11
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
13
12
|
|
|
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}
|
|
13
|
+
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(root,metrics=false){super(),this.root=root,this.metricsEnabled=metrics,this.numCpus=os.availableParallelism();}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,{process:{workspace,status:"pending",logs:[]},child:null,errorTimer:null,restartTimer:null,startupTimer:null,lastGoodStatus:null,restartRetries:0,lastOutputAt:0,intentionalExit:false});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&&child.kill("SIGKILL");},5e3);child.on("close",()=>{clearTimeout(escalate),resolve2();}),child.kill("SIGTERM");}));}await Promise.all(waiting);}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()}),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
15
|
`)&&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.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;proc.logs.push(sanitizeForDisplay(line)),proc.logs.length>MAX_LOGS&&proc.logs.splice(0,proc.logs.length-MAX_LOGS),entry.lastOutputAt=Date.now(),proc.status==="idle"&&(proc.status=entry.lastGoodStatus??"ready");let{status,url}=parseLine(stripVTControlCharacters(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();}probeUrl(url){return new Promise(resolve2=>{let req=(url.startsWith("https")?https:http).get(url,{timeout:3e3},res=>{res.resume(),resolve2(true);});req.on("error",()=>resolve2(false)),req.on("timeout",()=>{req.destroy(),resolve2(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.restartRetries=MAX_RESTART_RETRIES+1;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.restartRetries=MAX_RESTART_RETRIES+1,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");}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=this.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;if(prev){let elapsedMs=now-prev.time;if(elapsedMs>0){let tickDelta=totalTicks-prev.ticks,elapsedSec=elapsedMs/1e3;cpuPercent=tickDelta/100/elapsedSec/this.numCpus*100,cpuPercent<0&&(cpuPercent=0);}}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=new Map,stats=new Map;for(let line of output.trim().split(`
|
|
17
|
-
`).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);}let changed=false;for(let[rootPid,name]of rootPids){let pids=this.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,pageSize=4096,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 stat=fs.readFileSync(`/proc/${pid}/stat`,"utf8"),closeParen=stat.lastIndexOf(")");if(closeParen===-1)continue;let fields=stat.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;if(Number.isNaN(ppid))continue;stats.set(pid,{utime,stime,rss});let kids=children.get(ppid);kids||(kids=[],children.set(ppid,kids)),kids.push(pid);}catch{}}return {children,stats}}collectDescendants(rootPid,children){let result=[],stack=[rootPid];for(;stack.length>0;){let pid=stack.pop();result.push(pid);let kids=children.get(pid);kids&&stack.push(...kids);}return result}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 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=typeof obj.scripts=="object"&&obj.scripts!==null&&!Array.isArray(obj.scripts)?obj.scripts:void 0,dependencies=typeof obj.dependencies=="object"&&obj.dependencies!==null&&!Array.isArray(obj.dependencies)?obj.dependencies:void 0;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));return [...workspaces].sort((a,b)=>{if(a.kind!==b.kind)return kindOrder[a.kind]-kindOrder[b.kind];let aDeps=a.deps.filter(d=>names.has(d)).length,bDeps=b.deps.filter(d=>names.has(d)).length;return aDeps-bDeps})}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));}function useRunner(options2){let{exit}=useApp(),[loading,setLoading]=useState(true),[processes,setProcesses]=useState([]),runnerRef=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:[]}))),runner.on("change",()=>{setProcesses(displayOrder.flatMap(name=>{let p=runner.get(name);return p?[p]:[]}));}),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);}),[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);},[]);return {processes,loading,stop,stopProcess,restartProcess}}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 j/k select s stop/start r restart q quit";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:[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}){let logLines=proc.logs.slice(-height),fillCount=height-logLines.length;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})]}),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(()=>Math.max(14,...processes.map(p=>p.workspace.name.length+2)),[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];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})]})}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:[jsx(Text,{color:colors.accent,children:"\u25D1 "}),jsx(Text,{color:colors.muted,children:"Discovering workspaces..."})]})]})}function App({options:options2}){let{processes,loading,stop,stopProcess,restartProcess}=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));}),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);
|
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hlidskjalf",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A Terminal User Interface for monitoring Turborepo tasks, built with Ink",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Charlie Beckstrand",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
25
|
"LICENSE",
|
|
26
|
-
"README.md"
|
|
26
|
+
"README.md",
|
|
27
|
+
"CHANGELOG.md"
|
|
27
28
|
],
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "tsup",
|
|
@@ -31,10 +32,13 @@
|
|
|
31
32
|
"lint": "biome check .",
|
|
32
33
|
"lint:fix": "biome check --write .",
|
|
33
34
|
"format": "biome format --write .",
|
|
34
|
-
"
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"bench": "tsx __benchmarks__/index.ts"
|
|
35
38
|
},
|
|
36
39
|
"dependencies": {
|
|
37
40
|
"ink": "^5.2.1",
|
|
41
|
+
"ink-spinner": "^5.0.0",
|
|
38
42
|
"react": "^18.3.1"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
@@ -42,7 +46,9 @@
|
|
|
42
46
|
"@types/node": "^22.15.33",
|
|
43
47
|
"@types/react": "^18.3.23",
|
|
44
48
|
"lefthook": "^2.1.4",
|
|
49
|
+
"tinybench": "^6.0.2",
|
|
45
50
|
"tsup": "^8.5.1",
|
|
51
|
+
"tsx": "^4.22.3",
|
|
46
52
|
"typescript": "^5.9.3",
|
|
47
53
|
"vitest": "^4.1.1"
|
|
48
54
|
}
|