@zeluizr/lattice 1.1.0 → 2.0.0

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 CHANGED
@@ -4,7 +4,7 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
5
  to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [1.1.0] — 2026-06-24
7
+ ## [2.0.0] — 2026-06-24
8
8
 
9
9
  ### Added
10
10
  - **GIT panel reports the host of each repo.** A new `HOST` column tags every
@@ -13,17 +13,31 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
13
13
  - **zgit server listing.** A footer line in the GIT panel lists the bare repos on
14
14
  the self-hosted zgit Docker container (`docker exec -u git <container> ls
15
15
  /repos`). Local-only, no network; fails soft (the line is hidden) when Docker
16
- or the container is unavailable. Container name via `--zgit` / config
17
- `zgitContainer` (default `zgit`).
16
+ or the container is unavailable. Container name via config `zgitContainer`
17
+ (default `zgit`).
18
+ - **GIT report and PROCESSES sit side by side** on wide terminals (the GIT
19
+ report takes the larger share); they stack when the terminal is too narrow.
18
20
 
19
21
  ### Changed
20
- - **The GIT panel is now cwd-independent.** It scans a fixed, configured list of
21
- paths instead of the parent of the current directory, so the report is the
22
- same in every terminal/tab. Each `--repos` entry may be a repo itself or a
22
+ - **The GIT panel is now cwd-independent.** It scans a fixed list of paths from
23
+ config (`repoRoots`) instead of the parent of the current directory, so the
24
+ report is identical in every terminal/tab. Each entry may be a repo itself or a
23
25
  folder whose subdirectories are repos; entries are merged and de-duplicated.
24
- - `--repos` now accepts a comma-separated list and is persisted to
25
- `~/.config/lattice/config.json` (`repoRoots`). With no flag and no saved list,
26
- it falls back to the parent of the cwd (previous behaviour).
26
+ - **The GIT panel lists only repos that need attention** — uncommitted changes,
27
+ or commits to push (ahead) / pull (behind) most recently committed first,
28
+ capped at the top 10. Clean, in-sync repos are not listed; when none are
29
+ pending it shows `✓ all N repos up to date`. Uncommitted changes are now
30
+ labelled **uncommitted** (was "dirty"), with `↑`/`↓` for push/pull.
31
+
32
+ ### Removed
33
+ - **Power monitoring and the sudo prompt.** lattice no longer runs
34
+ `powermetrics`, so it never asks for a password on launch. Temperatures and fan
35
+ speed are unaffected — they still come from the native SMC helper (no sudo).
36
+ - **All CLI flags.** lattice now takes no options: it always runs the full
37
+ dashboard using the settings saved in `~/.config/lattice/config.json` (language
38
+ is chosen on first run; theme, icons, repo list and zgit container are read
39
+ from the file). Removed `--no-power`, `--no-vtex`, `--repos`, `--zgit`,
40
+ `--interval`, `--procs`, `--icons`, `--lang` and `--theme`.
27
41
 
28
42
  ## [1.0.0] — 2026-06-24
29
43
 
package/dist/app.js CHANGED
@@ -8,18 +8,19 @@ import { GitCollector } from "./collectors/git.js";
8
8
  import { ZgitCollector } from "./collectors/zgit.js";
9
9
  import { readGpu } from "./collectors/gpu.js";
10
10
  import { readSensors } from "./collectors/sensors.js";
11
- import { PowerCollector } from "./collectors/power.js";
12
11
  import { TokenCollector } from "./collectors/tokens.js";
13
12
  import { readVtex } from "./collectors/vtex.js";
14
13
  import { coreCell, fmtTok, humanBytes, humanRate, sparkline, statusLevel } from "./format.js";
15
14
  const MAX_HIST = 60;
16
15
  const push = (h, v) => [...h, v].slice(-MAX_HIST);
16
+ const DEFAULT_INTERVAL = 1; // seconds between refreshes (adjust at runtime with +/-)
17
+ const PROCS = 12; // number of top processes to list
17
18
  function timeStr() {
18
19
  const d = new Date();
19
20
  return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
20
21
  }
21
22
  export function App(props) {
22
- const { t, pal, icon, usePower, useVtex, repoRoots, zgitContainer, topN } = props;
23
+ const { t, pal, icon, repoRoots, zgitContainer } = props;
23
24
  const { exit } = useApp();
24
25
  const [sys, setSys] = useState(null);
25
26
  const [disks, setDisks] = useState(null);
@@ -27,7 +28,6 @@ export function App(props) {
27
28
  const [zgit, setZgit] = useState(null);
28
29
  const [gpu, setGpu] = useState(null);
29
30
  const [sensors, setSensors] = useState(null);
30
- const [power, setPower] = useState(null);
31
31
  const [tokens, setTokens] = useState(null);
32
32
  const [vtex, setVtex] = useState(null);
33
33
  const [hCpu, setHCpu] = useState([]);
@@ -36,7 +36,7 @@ export function App(props) {
36
36
  const [hGpu, setHGpu] = useState([]);
37
37
  const [hTemp, setHTemp] = useState([]);
38
38
  const [paused, setPaused] = useState(false);
39
- const [interval, setIntervalState] = useState(props.interval);
39
+ const [interval, setIntervalState] = useState(DEFAULT_INTERVAL);
40
40
  const [now, setNow] = useState(timeStr());
41
41
  const [cols, setCols] = useState(process.stdout.columns || 100);
42
42
  const sysRef = useRef(null);
@@ -44,7 +44,6 @@ export function App(props) {
44
44
  const gitRef = useRef(null);
45
45
  const zgitRef = useRef(null);
46
46
  const tokRef = useRef(null);
47
- const powerRef = useRef(null);
48
47
  const pausedRef = useRef(false);
49
48
  const mounted = useRef(true);
50
49
  useEffect(() => {
@@ -53,15 +52,11 @@ export function App(props) {
53
52
  // ----- init collectors + power subprocess + clock -------------------------
54
53
  useEffect(() => {
55
54
  mounted.current = true;
56
- sysRef.current = new SystemCollector(topN);
55
+ sysRef.current = new SystemCollector(PROCS);
57
56
  disksRef.current = new DisksCollector();
58
57
  gitRef.current = new GitCollector(repoRoots);
59
58
  zgitRef.current = new ZgitCollector(zgitContainer);
60
59
  tokRef.current = new TokenCollector();
61
- if (usePower) {
62
- powerRef.current = new PowerCollector(Math.round(props.interval * 1000));
63
- powerRef.current.start();
64
- }
65
60
  const clock = setInterval(() => mounted.current && setNow(timeStr()), 1000);
66
61
  const onResize = () => setCols(process.stdout.columns || 100);
67
62
  process.stdout.on("resize", onResize);
@@ -69,7 +64,6 @@ export function App(props) {
69
64
  mounted.current = false;
70
65
  clearInterval(clock);
71
66
  process.stdout.off("resize", onResize);
72
- powerRef.current?.stop();
73
67
  };
74
68
  // eslint-disable-next-line react-hooks/exhaustive-deps
75
69
  }, []);
@@ -83,14 +77,12 @@ export function App(props) {
83
77
  readGpu(),
84
78
  readSensors(),
85
79
  ]);
86
- const p = powerRef.current ? powerRef.current.read() : null;
87
80
  if (!mounted.current)
88
81
  return;
89
82
  setSys(s);
90
83
  setDisks(d);
91
84
  setGpu(g);
92
85
  setSensors(st);
93
- setPower(p);
94
86
  setHCpu((h) => push(h, s.cpuTotal));
95
87
  setHMem((h) => push(h, s.memPercent));
96
88
  setHNet((h) => push(h, (s.netRecvBps + s.netSentBps) / 1024 / 1024));
@@ -104,7 +96,7 @@ export function App(props) {
104
96
  return;
105
97
  const [tk, vx, gt, zg] = await Promise.all([
106
98
  tokRef.current.read(),
107
- useVtex ? readVtex() : Promise.resolve(null),
99
+ readVtex(),
108
100
  gitRef.current ? gitRef.current.read() : Promise.resolve(null),
109
101
  zgitRef.current ? zgitRef.current.read() : Promise.resolve(null),
110
102
  ]);
@@ -114,7 +106,7 @@ export function App(props) {
114
106
  setVtex(vx);
115
107
  setGit(gt);
116
108
  setZgit(zg);
117
- }, [useVtex]);
109
+ }, []);
118
110
  useEffect(() => {
119
111
  refreshData();
120
112
  const id = setInterval(refreshData, Math.max(250, interval * 1000));
@@ -127,7 +119,6 @@ export function App(props) {
127
119
  }, [refreshAux]);
128
120
  useInput((input) => {
129
121
  if (input === "q") {
130
- powerRef.current?.stop();
131
122
  exit();
132
123
  }
133
124
  else if (input === "p") {
@@ -148,6 +139,12 @@ export function App(props) {
148
139
  const inner = (w) => Math.max(6, w - 4); // minus border (2) + padding (2)
149
140
  const w2 = inner(col2);
150
141
  const w3 = inner(col3);
142
+ // GIT + PROCESSES share a row on wide terminals; they stack when it's too
143
+ // narrow to split. GIT's columns are fixed-width, so it takes only what it
144
+ // needs (clamped) and PROCESSES inherits the slack — handy for long names.
145
+ const sideBySide = cols >= 92;
146
+ const gitW = Math.max(48, Math.min(58, Math.floor((cols - 2 * m) * 0.46)));
147
+ const procW = cols - 2 * m - gitW;
151
148
  const statColor = (lvl) => lvl === "ok" ? pal.green : lvl === "warn" ? pal.yellow : pal.red;
152
149
  const Stat = ({ value, warn, crit, metric }) => {
153
150
  const lvl = statusLevel(value, warn, crit);
@@ -170,8 +167,9 @@ export function App(props) {
170
167
  const util = gpu?.utilPct ?? 0;
171
168
  const diskRows = disks?.disks ?? [];
172
169
  const gitRepos = git?.repos ?? [];
170
+ const gitTotal = git?.total ?? 0;
173
171
  const zgitServer = zgit?.available ? zgit : null;
174
- const showGit = gitRepos.length > 0 || !!zgitServer;
172
+ const showGit = gitTotal > 0 || !!zgitServer;
175
173
  const hostLabel = (r) => r.hostKind === "github"
176
174
  ? "GitHub"
177
175
  : r.hostKind === "zgit"
@@ -192,14 +190,9 @@ export function App(props) {
192
190
  const modelExtra = modelItems.length > 2 ? ` +${modelItems.length - 2}` : "";
193
191
  const web = tokens && (tokens.webSearch || tokens.webFetch) ? ` · web ${tokens.webSearch}/${tokens.webFetch}` : "";
194
192
  const models = modelParts.length ? modelParts.join(" · ") + modelExtra + web : t("tokens.none");
195
- const powerParts = [];
196
- if (power && (power.cpuW != null || power.gpuW != null))
197
- powerParts.push(`${icon("power")} ${(power.cpuW ?? 0).toFixed(1)}+${(power.gpuW ?? 0).toFixed(1)}W`);
198
- else if (usePower)
199
- powerParts.push(`${icon("power")} ${t("temp.waitingSudo")}`);
200
- if (sensors?.fans?.length)
201
- powerParts.push(`${icon("fan")} ${sensors.fans[0].rpm.toFixed(0)} rpm`);
202
- const tempLine2 = powerParts.length ? powerParts.join(" · ") : t("temp.needsSudo");
193
+ const tempLine2 = sensors?.fans?.length
194
+ ? `${icon("fan")} ${sensors.fans[0].rpm.toFixed(0)} rpm`
195
+ : "";
203
196
  return (_jsxs(Box, { flexDirection: "column", width: cols, children: [_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsx(Text, { color: pal.purple, bold: true, children: "\u25C7 lattice" }), _jsx(Text, { color: pal.comment, children: paused ? t("paused") : t("subtitle") }), _jsxs(Text, { color: pal.comment, children: [icon("clock"), " ", now] })] }), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Panel, { title: `${icon("cpu")} ${t("panel.cpu")}`, color: pal.cyan, width: col2, children: [_jsxs(Text, { wrap: "truncate", children: [t("cpu.usage"), ": ", _jsxs(Text, { bold: true, children: [cpu.toFixed(0), "%"] }), " ", _jsx(Stat, { value: cpu, warn: 60, crit: 85, metric: "cpu" })] }), _jsxs(Text, { wrap: "truncate", children: [t("cpu.cores"), ": ", cores.length, " ", t("cpu.perCore"), ": ", cores.map(coreCell).join("")] }), _jsx(Text, { color: pal.cyan, wrap: "truncate", children: sparkline(hCpu, w2) }), _jsx(Text, { color: pal.comment, wrap: "truncate", children: caption(hCpu, (v) => `${v.toFixed(0)}%`) })] }), _jsxs(Panel, { title: `${icon("mem")} ${t("panel.mem")}`, color: pal.green, width: col2, children: [_jsxs(Text, { wrap: "truncate", children: [t("mem.ram"), ": ", _jsxs(Text, { bold: true, children: [memPct.toFixed(0), "%"] }), " ", _jsx(Stat, { value: memPct, warn: 75, crit: 90, metric: "mem" })] }), _jsx(Text, { wrap: "truncate", children: t("mem.used", {
204
197
  used: humanBytes(sys?.memUsed),
205
198
  total: humanBytes(sys?.memTotal),
@@ -207,18 +200,18 @@ export function App(props) {
207
200
  }) }), _jsx(Text, { color: pal.green, wrap: "truncate", children: sparkline(hMem, w2) }), _jsx(Text, { color: pal.comment, wrap: "truncate", children: caption(hMem, (v) => `${v.toFixed(0)}%`) })] })] }), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Panel, { title: `${icon("temp")} ${t("panel.temp")}`, color: pal.orange, width: col3, children: [_jsx(Text, { wrap: "truncate", children: ct != null && gt != null ? (_jsxs(_Fragment, { children: [ct.toFixed(0), "\u00B0 / ", gt.toFixed(0), "\u00B0", " ", _jsx(Stat, { value: Math.max(ct, gt), warn: 65, crit: 80, metric: "temp" })] })) : ct != null || gt != null ? (_jsxs(_Fragment, { children: [(ct ?? gt ?? 0).toFixed(0), "\u00B0C", " ", _jsx(Stat, { value: ct ?? gt ?? 0, warn: 65, crit: 80, metric: "temp" })] })) : (_jsx(Text, { color: pal.comment, children: t("temp.unavailable") })) }), _jsx(Text, { wrap: "truncate", children: tempLine2 }), _jsx(Text, { color: pal.orange, wrap: "truncate", children: sparkline(hTemp, w3) }), _jsx(Text, { color: pal.comment, wrap: "truncate", children: caption(hTemp, (v) => `${v.toFixed(0)}°`) })] }), _jsxs(Panel, { title: `${icon("net")} ${t("panel.net")}`, color: pal.cyan, width: col3, children: [_jsxs(Text, { wrap: "truncate", children: ["\u2193 ", humanRate(sys?.netRecvBps)] }), _jsxs(Text, { wrap: "truncate", children: ["\u2191 ", humanRate(sys?.netSentBps)] }), _jsx(Text, { color: pal.cyan, wrap: "truncate", children: sparkline(hNet, w3) }), _jsx(Text, { color: pal.comment, wrap: "truncate", children: caption(hNet, (v) => humanRate(v * 1024 * 1024)) })] }), _jsxs(Panel, { title: `${icon("gpu")} ${t("panel.gpu")}`, color: pal.purple, width: col3, children: [_jsxs(Text, { wrap: "truncate", children: [t("gpu.usage"), ": ", _jsxs(Text, { bold: true, children: [util, "%"] }), " ", _jsx(Stat, { value: util, warn: 60, crit: 85, metric: "gpu" })] }), _jsx(Text, { wrap: "truncate", children: t("gpu.mem", { used: humanBytes(gpu?.memUsedBytes), alloc: humanBytes(gpu?.memAllocBytes) }) }), _jsx(Text, { color: pal.purple, wrap: "truncate", children: sparkline(hGpu, w3) }), _jsx(Text, { color: pal.comment, wrap: "truncate", children: caption(hGpu, (v) => `${v.toFixed(0)}%`) })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.cyan, paddingX: 1, marginRight: 1, width: fullW, children: [_jsxs(Text, { color: pal.cyan, bold: true, children: [icon("disk"), " ", t("panel.disks")] }), _jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("disks.mount").padEnd(22), t("disks.read").padEnd(12), t("disks.write").padEnd(12), t("disks.usage")] }), diskRows.length === 0 ? (_jsx(Text, { color: pal.comment, children: t("spark.collecting") })) : (diskRows.map((d) => {
208
201
  const lvl = statusLevel(d.usePercent, 80, 92);
209
202
  return (_jsxs(Text, { wrap: "truncate", children: [d.mount.slice(0, 21).padEnd(22), humanRate(d.readBps).padEnd(12), humanRate(d.writeBps).padEnd(12), _jsxs(Text, { color: statColor(lvl), children: ["\u25CF ", d.usePercent.toFixed(0), "%"] }), " ", _jsxs(Text, { color: pal.comment, children: ["(", humanBytes(d.usedBytes), "/", humanBytes(d.sizeBytes), ")"] })] }, d.mount));
210
- }))] }), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Panel, { title: `${icon("tokens")} ${t("panel.tokens")}`, color: pal.pink, width: useVtex ? col2 : fullW, children: [_jsx(Text, { wrap: "truncate", children: t("tokens.spent", { cost: (tokens?.cost ?? 0).toFixed(2), messages: tokens?.messages ?? 0 }) }), _jsx(Text, { wrap: "truncate", children: t("tokens.tokens", {
203
+ }))] }), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Panel, { title: `${icon("tokens")} ${t("panel.tokens")}`, color: pal.pink, width: col2, children: [_jsx(Text, { wrap: "truncate", children: t("tokens.spent", { cost: (tokens?.cost ?? 0).toFixed(2), messages: tokens?.messages ?? 0 }) }), _jsx(Text, { wrap: "truncate", children: t("tokens.tokens", {
211
204
  total: fmtTok(tokTotal),
212
205
  input: fmtTok(tokens?.input),
213
206
  output: fmtTok(tokens?.output),
214
- }) }), _jsx(Text, { wrap: "truncate", children: t("tokens.cache", { cw: fmtTok(tokens?.cacheW), cr: fmtTok(tokens?.cacheR) }) }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("tokens.byModel", { models }) })] }), useVtex && (_jsx(Panel, { title: `${icon("vtex")} ${t("panel.vtex")}`, color: pal.purple, width: col2, children: !vtex ? (_jsx(Text, { color: pal.comment, children: t("spark.collecting") })) : !vtex.installed ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.red, children: t("vtex.notInstalled") })] }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("vtex.install") })] })) : vtex.loggedIn ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.green, children: t("vtex.connected") })] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.account"), ": ", _jsx(Text, { bold: true, children: vtex.account })] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.user"), ": ", vtex.login || "—"] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.workspace"), ": ", vtex.workspace || "master"] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.yellow, children: t("vtex.notConnected") })] }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("vtex.signin") })] })) }))] }), showGit && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.green, paddingX: 1, marginRight: 1, width: fullW, children: [_jsxs(Text, { color: pal.green, bold: true, wrap: "truncate", children: [icon("git"), " ", t("panel.git"), git?.truncated ? _jsx(Text, { color: pal.comment, children: " (+)" }) : null] }), gitRepos.length > 0 && (_jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("git.repo").padEnd(20), t("git.branch").padEnd(18), t("git.state").padEnd(10), t("git.host")] })), gitRepos.map((r) => {
215
- const label = r.detached ? "(detached)" : r.branch;
216
- return (_jsxs(Text, { wrap: "truncate", children: [r.name.slice(0, 19).padEnd(20), label.slice(0, 17).padEnd(18), _jsx(Text, { color: r.dirty ? pal.yellow : pal.green, children: "\u25CF" }), ` ${r.dirty ? t("git.dirty") : t("git.clean")}`.padEnd(9), _jsx(Text, { color: hostColor(r), children: hostLabel(r).slice(0, 10).padEnd(10) }), r.ahead > 0 ? _jsxs(Text, { color: pal.comment, children: [" \u2191", r.ahead] }) : null, r.behind > 0 ? _jsxs(Text, { color: pal.comment, children: [" \u2193", r.behind] }) : null] }, r.path));
217
- }), zgitServer && (_jsx(Text, { color: pal.comment, wrap: "truncate", children: zgitServer.repos.length
218
- ? t("git.server", {
219
- container: zgitServer.container,
220
- list: zgitServer.repos.join(", "),
221
- n: zgitServer.repos.length,
222
- })
223
- : t("git.serverEmpty", { container: zgitServer.container }) }))] })), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.comment, paddingX: 1, marginRight: 1, width: fullW, children: [_jsxs(Text, { color: pal.comment, bold: true, children: [icon("proc"), " ", t("panel.procs")] }), _jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("proc.cpu").padEnd(6), t("proc.mem").padEnd(9), t("proc.pid").padEnd(8), t("proc.name")] }), (sys?.procs ?? []).map((p) => (_jsxs(Text, { wrap: "truncate", children: [`${p.cpu.toFixed(0)}%`.padEnd(6), humanBytes(p.rss).padEnd(9), String(p.pid).padEnd(8), p.name.slice(0, 30)] }, p.pid)))] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: pal.comment, children: [_jsx(Text, { color: pal.pink, children: "q" }), " ", t("key.quit"), " \u00B7 ", _jsx(Text, { color: pal.pink, children: "p" }), " ", t("key.pause"), " \u00B7", " ", _jsx(Text, { color: pal.pink, children: "+/-" }), " ", t("key.faster"), "/", t("key.slower"), " \u00B7 ", interval.toFixed(2), "s"] }) })] }));
207
+ }) }), _jsx(Text, { wrap: "truncate", children: t("tokens.cache", { cw: fmtTok(tokens?.cacheW), cr: fmtTok(tokens?.cacheR) }) }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("tokens.byModel", { models }) })] }), _jsx(Panel, { title: `${icon("vtex")} ${t("panel.vtex")}`, color: pal.purple, width: col2, children: !vtex ? (_jsx(Text, { color: pal.comment, children: t("spark.collecting") })) : !vtex.installed ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.red, children: t("vtex.notInstalled") })] }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("vtex.install") })] })) : vtex.loggedIn ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.green, children: t("vtex.connected") })] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.account"), ": ", _jsx(Text, { bold: true, children: vtex.account })] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.user"), ": ", vtex.login || "—"] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.workspace"), ": ", vtex.workspace || "master"] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.yellow, children: t("vtex.notConnected") })] }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("vtex.signin") })] })) })] }), _jsxs(Box, { flexDirection: sideBySide ? "row" : "column", alignItems: "flex-start", children: [showGit && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.green, paddingX: 1, marginRight: 1, width: sideBySide ? gitW : fullW, children: [_jsxs(Text, { color: pal.green, bold: true, wrap: "truncate", children: [icon("git"), " ", t("panel.git"), git?.truncated ? _jsx(Text, { color: pal.comment, children: " (+)" }) : null] }), gitRepos.length > 0 ? (_jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("git.repo").padEnd(18), t("git.branch").padEnd(12), t("git.host").padEnd(8), t("git.state")] })) : gitTotal > 0 ? (_jsx(Text, { color: pal.green, wrap: "truncate", children: t("git.allClear", { n: gitTotal }) })) : null, gitRepos.map((r) => {
208
+ const label = r.detached ? "(detached)" : r.branch;
209
+ return (_jsxs(Text, { wrap: "truncate", children: [r.name.slice(0, 17).padEnd(18), label.slice(0, 11).padEnd(12), _jsx(Text, { color: hostColor(r), children: hostLabel(r).slice(0, 7).padEnd(8) }), _jsx(Text, { color: r.dirty ? pal.yellow : pal.orange, children: "\u25CF" }), r.dirty ? _jsxs(Text, { color: pal.yellow, children: [" ", t("git.uncommitted")] }) : null, r.ahead > 0 ? _jsxs(Text, { color: pal.cyan, children: [" \u2191", r.ahead] }) : null, r.behind > 0 ? _jsxs(Text, { color: pal.orange, children: [" \u2193", r.behind] }) : null] }, r.path));
210
+ }), zgitServer && (_jsx(Text, { color: pal.comment, wrap: "truncate", children: zgitServer.repos.length
211
+ ? t("git.server", {
212
+ container: zgitServer.container,
213
+ list: zgitServer.repos.join(", "),
214
+ n: zgitServer.repos.length,
215
+ })
216
+ : t("git.serverEmpty", { container: zgitServer.container }) }))] })), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.comment, paddingX: 1, marginRight: 1, width: sideBySide && showGit ? procW : fullW, children: [_jsxs(Text, { color: pal.comment, bold: true, children: [icon("proc"), " ", t("panel.procs")] }), _jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("proc.cpu").padEnd(6), t("proc.mem").padEnd(9), t("proc.pid").padEnd(7), t("proc.name")] }), (sys?.procs ?? []).map((p) => (_jsxs(Text, { wrap: "truncate", children: [`${p.cpu.toFixed(0)}%`.padEnd(6), humanBytes(p.rss).padEnd(9), String(p.pid).padEnd(7), p.name.slice(0, 40)] }, p.pid)))] })] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: pal.comment, children: [_jsx(Text, { color: pal.pink, children: "q" }), " ", t("key.quit"), " \u00B7 ", _jsx(Text, { color: pal.pink, children: "p" }), " ", t("key.pause"), " \u00B7", " ", _jsx(Text, { color: pal.pink, children: "+/-" }), " ", t("key.faster"), "/", t("key.slower"), " \u00B7 ", interval.toFixed(2), "s"] }) })] }));
224
217
  }
package/dist/cli.js CHANGED
@@ -3,72 +3,34 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { dirname } from "node:path";
4
4
  import { render } from "ink";
5
5
  import meow from "meow";
6
- import { execa } from "execa";
7
6
  import { App } from "./app.js";
8
7
  import { LanguageSelect } from "./components/LanguageSelect.js";
9
8
  import { loadConfig, saveConfig } from "./config.js";
10
- import { detectLang, isLang, makeT } from "./i18n/index.js";
11
- import { isVariant, palette } from "./theme.js";
12
- import { isIconMode, makeIcons } from "./icons.js";
13
- const cli = meow(`
14
- ${"lattice"} real-time terminal dashboard for macOS Apple Silicon
9
+ import { detectLang, makeT } from "./i18n/index.js";
10
+ import { palette } from "./theme.js";
11
+ import { makeIcons } from "./icons.js";
12
+ // lattice takes no options and needs no sudo: it runs the full dashboard using
13
+ // the settings saved in ~/.config/lattice/config.json. Only --help / --version
14
+ // are handled here; language is chosen on first run, the rest lives in config.
15
+ meow(`
16
+ lattice — real-time terminal dashboard for macOS Apple Silicon
15
17
 
16
18
  Usage
17
- $ lattice [options]
18
-
19
- Options
20
- --no-power skip powermetrics/sudo (CPU/GPU/RAM/disk/net only)
21
- --no-vtex hide the VTEX panel (for non-VTEX users)
22
- --repos comma-separated folders and/or repos the GIT panel always
23
- shows, regardless of where lattice is launched. A path
24
- that is itself a repo is shown directly; otherwise its
25
- subdirectories are scanned. Saved to config once set.
26
- (default: saved list, else the parent of the current dir)
27
- --zgit Docker container of the self-hosted zgit server to list
28
- (default: zgit; the line is hidden when unavailable)
29
- --interval, -i refresh interval in seconds (default 1)
30
- --procs, -n number of top processes to show (default 8)
31
- --icons icon style: nerd | emoji | none (default nerd)
32
- --lang language: en | es | pt-BR (asked on first run)
33
- --theme pro | blade | buffy | lincoln | morbius | van-helsing
34
- --version, -v
35
- --help
36
-
37
- Examples
38
19
  $ lattice
39
- $ lattice --no-power --icons emoji
40
- $ lattice --lang es --theme blade
41
- `, {
42
- importMeta: import.meta,
43
- description: false,
44
- flags: {
45
- power: { type: "boolean", default: true },
46
- vtex: { type: "boolean", default: true },
47
- repos: { type: "string", default: "" },
48
- zgit: { type: "string", default: "" },
49
- interval: { type: "number", shortFlag: "i", default: 1 },
50
- procs: { type: "number", shortFlag: "n", default: 8 },
51
- icons: { type: "string", default: "" },
52
- lang: { type: "string", default: "" },
53
- theme: { type: "string", default: "" },
54
- },
55
- });
56
- async function resolveLang() {
57
- const cfg = loadConfig();
58
- const flag = cli.flags.lang;
59
- if (flag && isLang(flag)) {
60
- saveConfig({ lang: flag });
61
- return flag;
62
- }
63
- if (cfg.lang)
64
- return cfg.lang;
65
- // First run: ask, unless stdin isn't interactive.
20
+
21
+ lattice reads its language, theme, icons, repo list and zgit container from
22
+ ~/.config/lattice/config.json (language is chosen on first run).
23
+ `, { importMeta: import.meta, flags: {} });
24
+ /** Saved language, or the first-run picker (env-detected when non-interactive). */
25
+ async function resolveLang(variant) {
26
+ const saved = loadConfig().lang;
27
+ if (saved)
28
+ return saved;
66
29
  if (!process.stdin.isTTY) {
67
30
  const detected = detectLang();
68
31
  saveConfig({ lang: detected });
69
32
  return detected;
70
33
  }
71
- const variant = resolveTheme();
72
34
  const chosen = await new Promise((resolve) => {
73
35
  const app = render(_jsx(LanguageSelect, { pal: palette(variant), onSelect: (l) => {
74
36
  app.unmount();
@@ -78,69 +40,16 @@ async function resolveLang() {
78
40
  saveConfig({ lang: chosen });
79
41
  return chosen;
80
42
  }
81
- function resolveTheme() {
82
- const flag = cli.flags.theme;
83
- if (flag && isVariant(flag)) {
84
- saveConfig({ theme: flag });
85
- return flag;
86
- }
87
- return loadConfig().theme ?? "pro";
88
- }
89
- function resolveIcons() {
90
- const flag = cli.flags.icons;
91
- if (flag && isIconMode(flag)) {
92
- saveConfig({ icons: flag });
93
- return flag;
94
- }
95
- return loadConfig().icons ?? "nerd";
96
- }
97
- /** The fixed set of folders/repos the GIT panel scans — independent of cwd. */
98
- function resolveRepoRoots() {
99
- const flag = cli.flags.repos.trim();
100
- if (flag) {
101
- const roots = flag.split(",").map((s) => s.trim()).filter(Boolean);
102
- if (roots.length) {
103
- saveConfig({ repoRoots: roots });
104
- return roots;
105
- }
106
- }
107
- const saved = loadConfig().repoRoots;
108
- if (saved && saved.length)
109
- return saved;
110
- // First run / no config: fall back to the parent of the cwd so the tool still
111
- // works out of the box. Pass --repos once to pin a stable, cwd-independent list.
112
- return [dirname(process.cwd())];
113
- }
114
- function resolveZgitContainer() {
115
- const flag = cli.flags.zgit.trim();
116
- if (flag) {
117
- saveConfig({ zgitContainer: flag });
118
- return flag;
119
- }
120
- return loadConfig().zgitContainer ?? "zgit";
121
- }
122
43
  async function main() {
123
- const lang = await resolveLang();
124
- const variant = resolveTheme();
125
- const iconMode = resolveIcons();
126
- const repoRoots = resolveRepoRoots();
127
- const zgitContainer = resolveZgitContainer();
44
+ const cfg = loadConfig();
45
+ const variant = cfg.theme ?? "pro";
46
+ const lang = await resolveLang(variant);
128
47
  const t = makeT(lang);
129
48
  const pal = palette(variant);
130
- const icon = makeIcons(iconMode);
131
- let usePower = cli.flags.power;
132
- // Pre-authenticate sudo BEFORE the TUI takes over the terminal.
133
- if (usePower) {
134
- process.stdout.write(t("cli.sudoNeed") + "\n");
135
- try {
136
- await execa("sudo", ["-v"], { stdio: "inherit" });
137
- }
138
- catch {
139
- process.stdout.write(t("cli.sudoFail") + "\n");
140
- usePower = false;
141
- }
142
- }
143
- const { waitUntilExit } = render(_jsx(App, { t: t, pal: pal, icon: icon, lang: lang, usePower: usePower, useVtex: cli.flags.vtex, repoRoots: repoRoots, zgitContainer: zgitContainer, interval: cli.flags.interval, topN: cli.flags.procs }));
49
+ const icon = makeIcons(cfg.icons ?? "nerd");
50
+ const repoRoots = cfg.repoRoots?.length ? cfg.repoRoots : [dirname(process.cwd())];
51
+ const zgitContainer = cfg.zgitContainer ?? "zgit";
52
+ const { waitUntilExit } = render(_jsx(App, { t: t, pal: pal, icon: icon, repoRoots: repoRoots, zgitContainer: zgitContainer }));
144
53
  await waitUntilExit();
145
54
  }
146
55
  main().catch((e) => {
@@ -20,7 +20,8 @@ import { readdir } from "node:fs/promises";
20
20
  import { existsSync } from "node:fs";
21
21
  import { join, basename } from "node:path";
22
22
  const run = promisify(execFile);
23
- const MAX_REPOS = 24;
23
+ const MAX_REPOS = 48; // hard cap on how many repos we'll stat per refresh
24
+ const SHOW_REPOS = 10; // of those, only the most-recently-committed are reported
24
25
  const CONCURRENCY = 8;
25
26
  export class GitCollector {
26
27
  roots;
@@ -28,7 +29,7 @@ export class GitCollector {
28
29
  last;
29
30
  constructor(roots) {
30
31
  this.roots = roots;
31
- this.last = { roots, repos: [], truncated: false };
32
+ this.last = { roots, repos: [], total: 0, truncated: false };
32
33
  }
33
34
  async read() {
34
35
  if (this.inflight)
@@ -36,7 +37,6 @@ export class GitCollector {
36
37
  this.inflight = true;
37
38
  try {
38
39
  const dirs = await this.discover();
39
- const truncated = dirs.length > MAX_REPOS;
40
40
  const pick = dirs.slice(0, MAX_REPOS);
41
41
  const repos = [];
42
42
  for (let i = 0; i < pick.length; i += CONCURRENCY) {
@@ -46,8 +46,19 @@ export class GitCollector {
46
46
  if (r)
47
47
  repos.push(r);
48
48
  }
49
- repos.sort((a, b) => a.name.localeCompare(b.name));
50
- this.last = { roots: this.roots, repos, truncated };
49
+ // Only repos that need attention — uncommitted changes, or commits to
50
+ // push/pull most recently committed first, capped at SHOW_REPOS. Repos
51
+ // that are clean and in sync are counted but not listed.
52
+ const pending = repos
53
+ .filter((r) => r.dirty || r.ahead > 0 || r.behind > 0)
54
+ .sort((a, b) => b.lastCommit - a.lastCommit || a.name.localeCompare(b.name));
55
+ const shown = pending.slice(0, SHOW_REPOS);
56
+ this.last = {
57
+ roots: this.roots,
58
+ repos: shown,
59
+ total: repos.length,
60
+ truncated: pending.length > shown.length,
61
+ };
51
62
  return this.last;
52
63
  }
53
64
  catch {
@@ -87,11 +98,14 @@ export class GitCollector {
87
98
  }
88
99
  }
89
100
  async function readRepo(dir) {
90
- const [statusRes, urlRes] = await Promise.all([
101
+ const [statusRes, urlRes, dateRes] = await Promise.all([
91
102
  run("git", ["-C", dir, "status", "--porcelain=v2", "--branch"], { maxBuffer: 1 << 20 }),
92
103
  run("git", ["-C", dir, "remote", "get-url", "origin"], { maxBuffer: 1 << 16 }).catch(() => ({
93
104
  stdout: "",
94
105
  })),
106
+ run("git", ["-C", dir, "log", "-1", "--format=%ct"], { maxBuffer: 1 << 16 }).catch(() => ({
107
+ stdout: "",
108
+ })),
95
109
  ]);
96
110
  let branch = "?";
97
111
  let ahead = 0;
@@ -116,7 +130,8 @@ async function readRepo(dir) {
116
130
  }
117
131
  }
118
132
  const { host, hostKind } = classifyHost(urlRes.stdout.trim());
119
- return { name: basename(dir), path: dir, branch, ahead, behind, detached, dirty, host, hostKind };
133
+ const lastCommit = Number(dateRes.stdout.trim()) || 0;
134
+ return { name: basename(dir), path: dir, branch, ahead, behind, detached, dirty, host, hostKind, lastCommit };
120
135
  }
121
136
  /** Pull the hostname out of an origin URL and bucket it into a known host. */
122
137
  export function classifyHost(url) {
package/dist/i18n/en.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** English (base locale). Every other locale must implement these exact keys. */
2
2
  export const en = {
3
- "subtitle": "system · gpu · power",
3
+ "subtitle": "system · gpu · git",
4
4
  "paused": "PAUSED",
5
5
  "panel.cpu": "CPU",
6
6
  "panel.mem": "MEMORY",
@@ -40,16 +40,14 @@ export const en = {
40
40
  "git.branch": "BRANCH",
41
41
  "git.state": "STATE",
42
42
  "git.host": "HOST",
43
- "git.clean": "clean",
44
- "git.dirty": "dirty",
43
+ "git.uncommitted": "uncommitted",
44
+ "git.allClear": "✓ all {n} repos up to date",
45
45
  "git.local": "local",
46
46
  "git.server": "— zgit server ({container}): {list} ({n})",
47
47
  "git.serverEmpty": "— zgit server ({container}): no repos",
48
48
  "gpu.usage": "Usage",
49
49
  "gpu.mem": "mem {used}/{alloc}",
50
50
  "temp.unavailable": "sensors unavailable",
51
- "temp.waitingSudo": "waiting for sudo",
52
- "temp.needsSudo": "power: requires sudo",
53
51
  "spark.collecting": "collecting…",
54
52
  "spark.lastMin": "last min: {range}",
55
53
  "tokens.spent": "Spent today: ${cost} · {messages} messages",
@@ -74,7 +72,4 @@ export const en = {
74
72
  "key.pause": "Pause",
75
73
  "key.faster": "Faster",
76
74
  "key.slower": "Slower",
77
- "cli.sudoNeed": "lattice needs sudo to read power (powermetrics).",
78
- "cli.sudoFail": "sudo unavailable — continuing without power data.",
79
- "cli.sudoCancel": "cancelled.",
80
75
  };
package/dist/i18n/es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const es = {
2
- "subtitle": "sistema · gpu · energía",
2
+ "subtitle": "sistema · gpu · git",
3
3
  "paused": "PAUSADO",
4
4
  "panel.cpu": "CPU",
5
5
  "panel.mem": "MEMORIA",
@@ -39,16 +39,14 @@ export const es = {
39
39
  "git.branch": "BRANCH",
40
40
  "git.state": "ESTADO",
41
41
  "git.host": "HOST",
42
- "git.clean": "limpio",
43
- "git.dirty": "sucio",
42
+ "git.uncommitted": "sin commit",
43
+ "git.allClear": "✓ {n} repos al día",
44
44
  "git.local": "local",
45
45
  "git.server": "— servidor zgit ({container}): {list} ({n})",
46
46
  "git.serverEmpty": "— servidor zgit ({container}): sin repos",
47
47
  "gpu.usage": "Uso",
48
48
  "gpu.mem": "mem {used}/{alloc}",
49
49
  "temp.unavailable": "sensores no disponibles",
50
- "temp.waitingSudo": "esperando sudo",
51
- "temp.needsSudo": "energía: requiere sudo",
52
50
  "spark.collecting": "recolectando…",
53
51
  "spark.lastMin": "último min: {range}",
54
52
  "tokens.spent": "Gastado hoy: ${cost} · {messages} mensajes",
@@ -73,7 +71,4 @@ export const es = {
73
71
  "key.pause": "Pausar",
74
72
  "key.faster": "Más rápido",
75
73
  "key.slower": "Más lento",
76
- "cli.sudoNeed": "lattice necesita sudo para leer la energía (powermetrics).",
77
- "cli.sudoFail": "sudo no disponible — siguiendo sin datos de energía.",
78
- "cli.sudoCancel": "cancelado.",
79
74
  };
@@ -1,5 +1,5 @@
1
1
  export const ptBR = {
2
- "subtitle": "sistema · gpu · energia",
2
+ "subtitle": "sistema · gpu · git",
3
3
  "paused": "PAUSADO",
4
4
  "panel.cpu": "CPU",
5
5
  "panel.mem": "MEMÓRIA",
@@ -39,16 +39,14 @@ export const ptBR = {
39
39
  "git.branch": "BRANCH",
40
40
  "git.state": "ESTADO",
41
41
  "git.host": "HOST",
42
- "git.clean": "limpo",
43
- "git.dirty": "sujo",
42
+ "git.uncommitted": "sem commit",
43
+ "git.allClear": "✓ todos os {n} repos em dia",
44
44
  "git.local": "local",
45
45
  "git.server": "— servidor zgit ({container}): {list} ({n})",
46
46
  "git.serverEmpty": "— servidor zgit ({container}): sem repos",
47
47
  "gpu.usage": "Uso",
48
48
  "gpu.mem": "mem {used}/{alloc}",
49
49
  "temp.unavailable": "sensores indisponíveis",
50
- "temp.waitingSudo": "aguardando sudo",
51
- "temp.needsSudo": "energia: requer sudo",
52
50
  "spark.collecting": "coletando…",
53
51
  "spark.lastMin": "último min: {range}",
54
52
  "tokens.spent": "Gasto hoje: ${cost} · {messages} mensagens",
@@ -73,7 +71,4 @@ export const ptBR = {
73
71
  "key.pause": "Pausar",
74
72
  "key.faster": "Mais rápido",
75
73
  "key.slower": "Mais lento",
76
- "cli.sudoNeed": "lattice precisa de sudo para ler energia (powermetrics).",
77
- "cli.sudoFail": "sudo indisponível — seguindo sem dados de energia.",
78
- "cli.sudoCancel": "cancelado.",
79
74
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeluizr/lattice",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Real-time terminal dashboard for macOS Apple Silicon — GPU, power, temperatures/fans, per-disk I/O (incl. /Volumes), network, memory, processes, and AI token cost.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "terminal",
35
35
  "dashboard",
36
36
  "gpu",
37
- "powermetrics",
37
+ "git",
38
38
  "htop",
39
39
  "ink",
40
40
  "cli"
@@ -1,105 +0,0 @@
1
- /**
2
- * Power / frequency via `powermetrics` (REQUIRES sudo).
3
- *
4
- * powermetrics streams plist samples on stdout separated by a NUL byte (\x00).
5
- * We run it as a long-lived subprocess and keep the latest parsed sample. Power
6
- * fields arrive in milliwatts; we convert to watts.
7
- */
8
- import { spawn } from "node:child_process";
9
- import * as plist from "plist";
10
- import bplist from "bplist-parser";
11
- function parseSample(raw) {
12
- if (raw.length === 0)
13
- return null;
14
- try {
15
- // binary plist?
16
- if (raw.length >= 6 && raw.subarray(0, 6).toString("latin1") === "bplist") {
17
- const arr = bplist.parseBuffer(raw);
18
- return arr?.[0] ?? null;
19
- }
20
- // XML plist
21
- const text = raw.toString("utf8");
22
- const start = text.indexOf("<?xml");
23
- if (start === -1)
24
- return null;
25
- return plist.parse(text.slice(start));
26
- }
27
- catch {
28
- return null;
29
- }
30
- }
31
- function mwToW(d, key) {
32
- const v = d?.[key];
33
- return typeof v === "number" ? v / 1000 : null;
34
- }
35
- export class PowerCollector {
36
- intervalMs;
37
- latest = null;
38
- error = null;
39
- proc = null;
40
- buf = Buffer.alloc(0);
41
- stopped = false;
42
- constructor(intervalMs = 1000) {
43
- this.intervalMs = intervalMs;
44
- }
45
- start() {
46
- try {
47
- this.proc = spawn("sudo", [
48
- "powermetrics",
49
- "--samplers",
50
- "cpu_power,gpu_power,thermal",
51
- "-i",
52
- String(this.intervalMs),
53
- "-f",
54
- "plist",
55
- ], { stdio: ["ignore", "pipe", "ignore"] });
56
- }
57
- catch (e) {
58
- this.error = `failed to start powermetrics: ${e}`;
59
- return;
60
- }
61
- this.proc.stdout?.on("data", (chunk) => {
62
- if (this.stopped)
63
- return;
64
- this.buf = Buffer.concat([this.buf, chunk]);
65
- let nul;
66
- while ((nul = this.buf.indexOf(0x00)) !== -1) {
67
- const raw = this.buf.subarray(0, nul);
68
- this.buf = this.buf.subarray(nul + 1);
69
- const sample = parseSample(raw);
70
- if (sample)
71
- this.latest = sample;
72
- }
73
- });
74
- this.proc.on("error", (e) => {
75
- this.error = String(e);
76
- });
77
- }
78
- read() {
79
- const d = this.latest;
80
- if (!d)
81
- return null;
82
- const proc = d.processor ?? {};
83
- const gpu = d.gpu ?? {};
84
- const freqHz = gpu.freq_hz;
85
- return {
86
- cpuW: mwToW(proc, "cpu_power"),
87
- gpuW: mwToW(proc, "gpu_power"),
88
- aneW: mwToW(proc, "ane_power"),
89
- packageW: mwToW(proc, "combined_power") ?? mwToW(proc, "package_power"),
90
- gpuFreqMhz: typeof freqHz === "number" ? freqHz / 1e6 : null,
91
- thermal: d.thermal_pressure ?? null,
92
- };
93
- }
94
- stop() {
95
- this.stopped = true;
96
- if (this.proc) {
97
- try {
98
- this.proc.kill("SIGTERM");
99
- }
100
- catch {
101
- // ignore
102
- }
103
- }
104
- }
105
- }