hlidskjalf 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +15 -14
- package/dist/chunk-P7MGC72I.js +11 -0
- package/dist/config.d.ts +7 -11
- package/dist/config.js +1 -1
- package/dist/index.js +24 -24
- package/package.json +4 -2
- package/dist/chunk-WSO44QZR.js +0 -10
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ 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.4.3]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Pause / resume and force-kill controls** — press `p` to suspend the selected
|
|
13
|
+
workspace with `SIGSTOP` (it stays alive and holds its port but consumes no CPU)
|
|
14
|
+
and again to resume it with `SIGCONT`, restoring the status it held before
|
|
15
|
+
pausing; a new `paused` status shows in amber. Press `x` to force-kill a
|
|
16
|
+
wedged workspace with `SIGKILL` immediately — no graceful `SIGTERM` grace period
|
|
17
|
+
and no restart. A paused process is woken before any stop/restart/kill so the
|
|
18
|
+
signal lands promptly.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Lighter redraws under heavy output** — the dashboard now skips re-rendering
|
|
23
|
+
rows and the log panel whose content hasn't changed, so a flood of output from
|
|
24
|
+
one workspace no longer repaints every other row.
|
|
25
|
+
- **Header status dot reads three states at a glance** — green when every
|
|
26
|
+
workspace is up, green and hollow when some are up and some stopped, and amber
|
|
27
|
+
when any is paused; hollow grey when nothing is running.
|
|
28
|
+
- **Consistent process-control log notes** — the stop, restart, kill, and pause
|
|
29
|
+
messages now share one wording and each shows its signal, and a stop or kill
|
|
30
|
+
appends a line when the child has actually exited, not just when requested.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Quitting with a paused workspace no longer hangs** — a suspended process is
|
|
35
|
+
woken so it acts on the shutdown signal at once, instead of waiting out the
|
|
36
|
+
five-second kill grace period.
|
|
37
|
+
- **Stopping a workspace no longer logs a phantom failure** — the dev runner's
|
|
38
|
+
`ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL` / "command failed with signal SIGTERM"
|
|
39
|
+
complaint, emitted as it forwards the stop we sent, is dropped rather than
|
|
40
|
+
shown as an error the user didn't cause.
|
|
41
|
+
- **Status glyphs sit on the baseline** — the half-circle and pause glyphs were
|
|
42
|
+
pulled from an oversized fallback font that floated them above the text;
|
|
43
|
+
indicators now stay within glyphs the primary font renders in line.
|
|
44
|
+
|
|
45
|
+
## [0.4.2]
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **Boolean flags take a value** — disable file watching with `--watch=false` and
|
|
50
|
+
metrics with `--metrics=false` (both also accept `=true`, and a bare `--watch` /
|
|
51
|
+
`--metrics` still reads as `true`). Replaces the `--no-watch` / `--no-metrics`
|
|
52
|
+
forms. The config forms (`watch: false`, `metrics: true`) are unchanged.
|
|
53
|
+
|
|
8
54
|
## [0.4.1]
|
|
9
55
|
|
|
10
56
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
A Terminal User Interface for visualizing Turborepo tasks, built with [Ink](https://npm.im/ink).
|
|
8
8
|
|
|
@@ -30,12 +30,12 @@ pnpm dev
|
|
|
30
30
|
|
|
31
31
|
| Option | Description |
|
|
32
32
|
| --- | --- |
|
|
33
|
-
| `filter` | Include specific workspaces (`--filter=web`). Append `...` for transitive
|
|
34
|
-
| `order` | Sort by `alphabetical`
|
|
35
|
-
| `title` | Custom title
|
|
33
|
+
| `filter` | Include specific workspaces (`--filter=web`). Append `...` for transitive dependencies (`--filter=web...`). |
|
|
34
|
+
| `order` | Sort by `alphabetical` or `run` order (`--order=run`). Defaults to `alphabetical`. |
|
|
35
|
+
| `title` | Custom title (`--title="My App"`). Defaults to `Hlidskjalf`. |
|
|
36
|
+
| `theme` | Colour theme (`--theme=niflheim` or `--theme=ice`). Defaults to `bifrost`. |
|
|
36
37
|
| `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 `--
|
|
38
|
-
| `theme` | Colour theme (`--theme=niflheim` or `--theme=ice`). One of `bifrost` (default), `niflheim`, `muspelheim`, and `yggdrasil`. |
|
|
38
|
+
| `watch` | Re-discover workspaces when `package.json` files change. Defaults to `true`; disable with `--watch=false`. |
|
|
39
39
|
|
|
40
40
|
## Themes
|
|
41
41
|
|
|
@@ -43,7 +43,7 @@ Named for the realms of Norse cosmology, to match the all-seeing high seat the t
|
|
|
43
43
|
|
|
44
44
|
| Theme | Alias | Mood |
|
|
45
45
|
| --- | --- | --- |
|
|
46
|
-
| `bifrost` | — | Default -
|
|
46
|
+
| `bifrost` | — | Default - electric purples, sky blues, and starlight whites. |
|
|
47
47
|
| `niflheim` | `ice` | Ice — glacial blues, frost-white highlights. |
|
|
48
48
|
| `muspelheim` | `fire` | Fire — molten oranges, ember golds. |
|
|
49
49
|
| `yggdrasil` | `earth` | Earth — mosses, leaf-greens, bark greys. |
|
|
@@ -51,11 +51,9 @@ Named for the realms of Norse cosmology, to match the all-seeing high seat the t
|
|
|
51
51
|
Each realm also answers to its elemental alias, so `--theme=ice` is the same as
|
|
52
52
|
`--theme=niflheim`.
|
|
53
53
|
|
|
54
|
-
Status colours (running / warning / error) stay legible in every theme, so a glyph never misreads.
|
|
55
|
-
|
|
56
54
|
## Configuration
|
|
57
55
|
|
|
58
|
-
Persist any of the options above
|
|
56
|
+
Persist any of the options above.
|
|
59
57
|
Create a `hlidskjalf.config.ts` at the repo root:
|
|
60
58
|
|
|
61
59
|
```ts
|
|
@@ -69,8 +67,7 @@ export default defineConfig({
|
|
|
69
67
|
})
|
|
70
68
|
```
|
|
71
69
|
|
|
72
|
-
`defineConfig` is optional — a plain `export default { ... }` works too
|
|
73
|
-
`hlidskjalf.config.js` / `hlidskjalf.config.mjs` are also recognized. The `.ts`
|
|
70
|
+
`defineConfig` is optional — a plain `export default { ... }` works too. `hlidskjalf.config.js` / `hlidskjalf.config.mjs` are also recognized. The `.ts`
|
|
74
71
|
form needs no build step: it's loaded directly via Node's type stripping
|
|
75
72
|
(Node ≥ 22.18).
|
|
76
73
|
|
|
@@ -93,18 +90,22 @@ defaults**, so a flag always wins over a stored value.
|
|
|
93
90
|
While running, hlidskjalf watches your `packages`, `apps`, and `services`
|
|
94
91
|
directories. When a workspace's `package.json` is added, removed, or changed it
|
|
95
92
|
re-runs discovery: new workspaces start automatically and removed ones are
|
|
96
|
-
stopped and dropped from the dashboard. Pass `--
|
|
93
|
+
stopped and dropped from the dashboard. Pass `--watch=false` (or set
|
|
97
94
|
`watch: false`) to turn this off.
|
|
98
95
|
|
|
99
96
|
## Controls
|
|
100
97
|
|
|
101
98
|
| Key | Action |
|
|
102
99
|
| --- | --- |
|
|
103
|
-
| `↑` / `↓` | Move the selection between workspaces |
|
|
100
|
+
| `↑` / `↓` (or `k` / `j`) | Move the selection between workspaces |
|
|
104
101
|
| `s` | Stop the selected workspace (or start it again if stopped) |
|
|
102
|
+
| `p` | Pause the selected workspace with `SIGSTOP` (or resume it with `SIGCONT` if paused) |
|
|
103
|
+
| `x` | Force-kill the selected workspace with `SIGKILL`, without restarting it |
|
|
105
104
|
| `r` | Restart the selected workspace |
|
|
106
105
|
| `c` | Clear the logs for the selected workspace |
|
|
107
106
|
| `PgUp` / `PgDn` | Scroll the log panel up / down a page |
|
|
107
|
+
| `Home` / `End` | Jump to the oldest / newest log line |
|
|
108
|
+
| `?` | Toggle help |
|
|
108
109
|
| `q` | Quit |
|
|
109
110
|
|
|
110
111
|
## License
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { existsSync, readdirSync, realpathSync, readFileSync } from 'fs';
|
|
4
|
+
import { resolve, join, sep } from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { stripVTControlCharacters } from 'util';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
9
|
+
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:/⚡️?\s*Build success/,status:"watching"},{pattern:/Build start/,status:"building"},{pattern:/Watching for changes/,status:"watching"},{pattern:/\[ERROR\]/,status:"error"},{pattern:/(?<!\/)error[\s:]/i,status:"error"},{pattern:/process exit/,status:"error"},{pattern:/\blistening\b/i,status:"ready"}],matchers=baseMatchers.map(m=>({...m,needsHttp:m.pattern.source.includes("http")})),ANY_NON_HTTP_MATCHER=new RegExp(matchers.filter(m=>!m.needsHttp).map(m=>`(?:${m.pattern.source})`).join("|"),"i");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");if(!hasHttp&&!ANY_NON_HTTP_MATCHER.test(truncated))return {};for(let{pattern,status,needsHttp}of matchers){if(needsHttp&&!hasHttp)continue;let match=truncated.match(pattern);if(match){let url=match[1]?localOrigin(match[1]):void 0;return {status,url}}}return {}}function stripAnsi(text){return text.includes("\x1B")?stripVTControlCharacters(text):text}var NON_SGR_ESCAPES=/\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|\[[?>=]*[\d;]*[A-Za-ln-z@~`]|\([A-Za-z]|[^[(\]\x1b])/g,BARE_CONTROLS=/[\x00-\x08\x0b-\x1a\x1c-\x1f\x7f]/g;function sanitizeForDisplay(text){let hasEscape=text.includes("\x1B"),hasControl=text.search(BARE_CONTROLS)!==-1;if(!hasEscape&&!hasControl)return text;let out=text;return hasEscape&&(out=out.replace(NON_SGR_ESCAPES,"")),hasControl&&(out=out.replace(BARE_CONTROLS,"")),out}var SHARED={success:"#15FA5A",warning:"#FACC15",error:"#F87171",pending:"#8D93A0",highlight:"#faf9f6",muted:"#8D93A0",dim:"#6B7280",separator:"#353940"},themes={bifrost:{accent:"#7C8EF2",accentBright:"#BEC7F9",url:"#E8EBFD",...SHARED},niflheim:{accent:"#22D3EE",accentBright:"#92E9F7",url:"#E7FAFD",...SHARED},muspelheim:{accent:"#F97316",accentBright:"#FCBB8D",url:"#FEF0E6",...SHARED},yggdrasil:{accent:"#22C55E",accentBright:"#73E79E",url:"#E9FBF0",...SHARED}},THEME_ALIASES={ice:"niflheim",fire:"muspelheim",earth:"yggdrasil"},DEFAULT_THEME="bifrost";function parseTheme(value){if(typeof value=="string")return value in themes?value:value in THEME_ALIASES?THEME_ALIASES[value]:void 0}var colors=themes[DEFAULT_THEME];function buildStatusDisplay(c){return {pending:{color:c.pending,label:"pending",icon:"\u25CB"},building:{color:c.warning,label:"building",icon:"\u25CB"},watching:{color:c.success,label:"watching",icon:"\u25CF"},ready:{color:c.success,label:"watching",icon:"\u25CF"},error:{color:c.error,label:"error",icon:"\u2716"},stopped:{color:c.pending,label:"stopped",icon:"\u25CB"},idle:{color:c.warning,label:"idle",icon:"\u25CB"},paused:{color:c.warning,label:"paused",icon:"\u25CB"},timeout:{color:c.error,label:"timeout",icon:"\u2716"}}}var statusDisplay=buildStatusDisplay(colors);function setTheme(name){colors=themes[name],statusDisplay=buildStatusDisplay(colors);}function formatCpu(cpu){return `${cpu.toFixed(1)}%`.padStart(6)}function formatMem(bytes){let formatted;return bytes<1024*1024?formatted=`${(bytes/1024).toFixed(0)} K`:bytes<1024*1024*1024?formatted=`${(bytes/(1024*1024)).toFixed(1)} M`:formatted=`${(bytes/(1024*1024*1024)).toFixed(1)} G`,formatted.padStart(7)}function memColor(bytes){return bytes>512*1024*1024?colors.error:bytes>256*1024*1024?colors.warning:colors.muted}function cpuColor(cpu){return cpu>80?colors.error:colors.muted}var HINTS="\u2191/\u2193 navigate | ? help | q quit";var OSC8="\x1B]8;;";function hyperlink(url,label=url){return `${OSC8}${url}\x07${label}${OSC8}\x07`}function truncateEnd(text,width){return width<=0?"":text.length<=width?text:width===1?"\u2026":`${text.slice(0,width-1)}\u2026`}var ENTER_ALT_SCREEN="\x1B[?1049h",EXIT_ALT_SCREEN="\x1B[?1049l";function enterAltScreen(stream=process.stdout){if(!stream.isTTY)return ()=>{};stream.write(ENTER_ALT_SCREEN);let restored=false,restore=()=>{restored||(restored=true,stream.write(EXIT_ALT_SCREEN));};return process.once("exit",restore),restore}function every(ms,fn){let timer=setInterval(fn,ms);return timer.unref(),()=>clearInterval(timer)}function isPlainObject(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}var VALID_PKG_NAME=/^(@[a-z0-9\-~][a-z0-9\-._~]*\/)?[a-z0-9\-~][a-z0-9\-._~]*$/;function isValidPackageName(name){return VALID_PKG_NAME.test(name)&&name.length<=214}function normalizeFilters(raw){return raw.map(v=>v.replace(/^\{(.+)\}$/,"$1")).filter(v=>{let name=v.endsWith("...")?v.slice(0,-3):v;return isValidPackageName(name)?true:(console.error(`Ignoring invalid filter: ${sanitizeForDisplay(name)}`),false)})}function stringRecord(value){if(!isPlainObject(value))return;let result={};for(let[key,v]of Object.entries(value))typeof v=="string"&&(result[key]=v);return result}function readJson(path){try{let raw=JSON.parse(readFileSync(path,"utf-8"));return isPlainObject(raw)?{name:typeof raw.name=="string"?raw.name:void 0,scripts:stringRecord(raw.scripts),dependencies:stringRecord(raw.dependencies)}:null}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));}function defineConfig(config){return config}var CONFIG_FILES=["hlidskjalf.config.ts","hlidskjalf.config.mjs","hlidskjalf.config.js"],PACKAGE_JSON_KEY="hlidskjalf";function validate(raw,source){if(!isPlainObject(raw))return console.error(`Ignoring ${source}: expected a config object.`),{};let config={};if(Array.isArray(raw.filter)){let strings=raw.filter.filter(v=>typeof v=="string"),filter=normalizeFilters(strings);filter.length&&(config.filter=filter);}(raw.order==="run"||raw.order==="alphabetical")&&(config.order=raw.order),typeof raw.title=="string"&&(config.title=sanitizeForDisplay(raw.title)),typeof raw.metrics=="boolean"&&(config.metrics=raw.metrics),typeof raw.watch=="boolean"&&(config.watch=raw.watch);let theme=parseTheme(raw.theme);return theme&&(config.theme=theme),config}function fromPackageJson(root){let path=join(root,"package.json");if(!existsSync(path))return {};let parsed;try{parsed=JSON.parse(readFileSync(path,"utf-8"));}catch{return {}}if(!isPlainObject(parsed))return {};let key=parsed[PACKAGE_JSON_KEY];return key===void 0?{}:validate(key,`package.json "${PACKAGE_JSON_KEY}" key`)}async function fromConfigFile(root){for(let name of CONFIG_FILES){let path=join(root,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(root){let fromPkg=fromPackageJson(root),fromFile=await fromConfigFile(root);return {...fromPkg,...fromFile}}
|
|
10
|
+
|
|
11
|
+
export { DEFAULT_THEME, HINTS, THEME_ALIASES, colors, cpuColor, defineConfig, discover, enterAltScreen, every, filterWorkspaces, formatCpu, formatMem, hyperlink, loadConfig, memColor, normalizeFilters, parseLine, parseTheme, sanitizeForDisplay, setTheme, sortByDeps, sortByName, statusDisplay, stripAnsi, themes, truncateEnd };
|
package/dist/config.d.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* primitives the dashboard leans on (OSC 8 hyperlinks, alternate screen), value
|
|
4
|
-
* formatters, and the timer helpers used across the polling code. Kept free of
|
|
5
|
-
* Ink/React imports so every helper here can be unit tested directly.
|
|
2
|
+
* Colour palette and status glyphs. No Ink/React imports, so every helper is unit-testable.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Built-in palettes, named for the realms of Norse cosmology (fitting for a tool named
|
|
10
|
-
* after Odin's all-seeing high seat). `success`/`warning`/`error` stay
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* slots pulled from {@link SHARED}.
|
|
7
|
+
* after Odin's all-seeing high seat). `success`/`warning`/`error` stay green/amber/red
|
|
8
|
+
* across every theme so a status glyph never misreads; personality lives in the accent
|
|
9
|
+
* colours, with neutral slots pulled from {@link SHARED}.
|
|
14
10
|
*/
|
|
15
11
|
declare const themes: {
|
|
16
12
|
readonly bifrost: {
|
|
@@ -93,9 +89,9 @@ interface Config {
|
|
|
93
89
|
/** Re-discover workspaces when `package.json` files change. Defaults to `true`. */
|
|
94
90
|
watch?: boolean;
|
|
95
91
|
/**
|
|
96
|
-
* Colour theme. Defaults to `bifrost` (
|
|
97
|
-
* (`niflheim`, `muspelheim`, `yggdrasil`) or an elemental alias (`ice`, `fire`,
|
|
98
|
-
* See {@link themes} for the palettes
|
|
92
|
+
* Colour theme. Defaults to `bifrost` (electric purples and sky blues). Accepts a realm
|
|
93
|
+
* name (`niflheim`, `muspelheim`, `yggdrasil`) or an elemental alias (`ice`, `fire`,
|
|
94
|
+
* `earth`). See {@link themes} for the available palettes.
|
|
99
95
|
*/
|
|
100
96
|
theme?: ThemeName;
|
|
101
97
|
}
|
package/dist/config.js
CHANGED