@zeluizr/lattice 1.1.0 → 2.1.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 +23 -9
- package/dist/app.js +45 -38
- package/dist/cli.js +24 -115
- package/dist/collectors/git.js +22 -7
- package/dist/collectors/huggingface.js +170 -0
- package/dist/config.js +2 -0
- package/dist/format.js +14 -0
- package/dist/i18n/en.js +13 -8
- package/dist/i18n/es.js +13 -8
- package/dist/i18n/pt-BR.js +13 -8
- package/dist/icons.js +2 -0
- package/package.json +2 -2
- package/dist/collectors/power.js +0 -105
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
|
-
## [
|
|
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
|
|
17
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
@@ -6,20 +6,22 @@ import { SystemCollector } from "./collectors/system.js";
|
|
|
6
6
|
import { DisksCollector } from "./collectors/disks.js";
|
|
7
7
|
import { GitCollector } from "./collectors/git.js";
|
|
8
8
|
import { ZgitCollector } from "./collectors/zgit.js";
|
|
9
|
+
import { HFCollector } from "./collectors/huggingface.js";
|
|
9
10
|
import { readGpu } from "./collectors/gpu.js";
|
|
10
11
|
import { readSensors } from "./collectors/sensors.js";
|
|
11
|
-
import { PowerCollector } from "./collectors/power.js";
|
|
12
12
|
import { TokenCollector } from "./collectors/tokens.js";
|
|
13
13
|
import { readVtex } from "./collectors/vtex.js";
|
|
14
|
-
import { coreCell, fmtTok, humanBytes, humanRate, sparkline, statusLevel } from "./format.js";
|
|
14
|
+
import { agoShort, coreCell, fmtTok, humanBytes, humanRate, sparkline, statusLevel } from "./format.js";
|
|
15
15
|
const MAX_HIST = 60;
|
|
16
16
|
const push = (h, v) => [...h, v].slice(-MAX_HIST);
|
|
17
|
+
const DEFAULT_INTERVAL = 1; // seconds between refreshes (adjust at runtime with +/-)
|
|
18
|
+
const PROCS = 12; // number of top processes to list
|
|
17
19
|
function timeStr() {
|
|
18
20
|
const d = new Date();
|
|
19
21
|
return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
20
22
|
}
|
|
21
23
|
export function App(props) {
|
|
22
|
-
const { t, pal, icon,
|
|
24
|
+
const { t, pal, icon, repoRoots, zgitContainer, hfCachePath } = props;
|
|
23
25
|
const { exit } = useApp();
|
|
24
26
|
const [sys, setSys] = useState(null);
|
|
25
27
|
const [disks, setDisks] = useState(null);
|
|
@@ -27,16 +29,16 @@ export function App(props) {
|
|
|
27
29
|
const [zgit, setZgit] = useState(null);
|
|
28
30
|
const [gpu, setGpu] = useState(null);
|
|
29
31
|
const [sensors, setSensors] = useState(null);
|
|
30
|
-
const [power, setPower] = useState(null);
|
|
31
32
|
const [tokens, setTokens] = useState(null);
|
|
32
33
|
const [vtex, setVtex] = useState(null);
|
|
34
|
+
const [hf, setHf] = useState(null);
|
|
33
35
|
const [hCpu, setHCpu] = useState([]);
|
|
34
36
|
const [hMem, setHMem] = useState([]);
|
|
35
37
|
const [hNet, setHNet] = useState([]);
|
|
36
38
|
const [hGpu, setHGpu] = useState([]);
|
|
37
39
|
const [hTemp, setHTemp] = useState([]);
|
|
38
40
|
const [paused, setPaused] = useState(false);
|
|
39
|
-
const [interval, setIntervalState] = useState(
|
|
41
|
+
const [interval, setIntervalState] = useState(DEFAULT_INTERVAL);
|
|
40
42
|
const [now, setNow] = useState(timeStr());
|
|
41
43
|
const [cols, setCols] = useState(process.stdout.columns || 100);
|
|
42
44
|
const sysRef = useRef(null);
|
|
@@ -44,7 +46,7 @@ export function App(props) {
|
|
|
44
46
|
const gitRef = useRef(null);
|
|
45
47
|
const zgitRef = useRef(null);
|
|
46
48
|
const tokRef = useRef(null);
|
|
47
|
-
const
|
|
49
|
+
const hfRef = useRef(null);
|
|
48
50
|
const pausedRef = useRef(false);
|
|
49
51
|
const mounted = useRef(true);
|
|
50
52
|
useEffect(() => {
|
|
@@ -53,15 +55,12 @@ export function App(props) {
|
|
|
53
55
|
// ----- init collectors + power subprocess + clock -------------------------
|
|
54
56
|
useEffect(() => {
|
|
55
57
|
mounted.current = true;
|
|
56
|
-
sysRef.current = new SystemCollector(
|
|
58
|
+
sysRef.current = new SystemCollector(PROCS);
|
|
57
59
|
disksRef.current = new DisksCollector();
|
|
58
60
|
gitRef.current = new GitCollector(repoRoots);
|
|
59
61
|
zgitRef.current = new ZgitCollector(zgitContainer);
|
|
60
62
|
tokRef.current = new TokenCollector();
|
|
61
|
-
|
|
62
|
-
powerRef.current = new PowerCollector(Math.round(props.interval * 1000));
|
|
63
|
-
powerRef.current.start();
|
|
64
|
-
}
|
|
63
|
+
hfRef.current = new HFCollector(hfCachePath);
|
|
65
64
|
const clock = setInterval(() => mounted.current && setNow(timeStr()), 1000);
|
|
66
65
|
const onResize = () => setCols(process.stdout.columns || 100);
|
|
67
66
|
process.stdout.on("resize", onResize);
|
|
@@ -69,7 +68,6 @@ export function App(props) {
|
|
|
69
68
|
mounted.current = false;
|
|
70
69
|
clearInterval(clock);
|
|
71
70
|
process.stdout.off("resize", onResize);
|
|
72
|
-
powerRef.current?.stop();
|
|
73
71
|
};
|
|
74
72
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
73
|
}, []);
|
|
@@ -83,14 +81,12 @@ export function App(props) {
|
|
|
83
81
|
readGpu(),
|
|
84
82
|
readSensors(),
|
|
85
83
|
]);
|
|
86
|
-
const p = powerRef.current ? powerRef.current.read() : null;
|
|
87
84
|
if (!mounted.current)
|
|
88
85
|
return;
|
|
89
86
|
setSys(s);
|
|
90
87
|
setDisks(d);
|
|
91
88
|
setGpu(g);
|
|
92
89
|
setSensors(st);
|
|
93
|
-
setPower(p);
|
|
94
90
|
setHCpu((h) => push(h, s.cpuTotal));
|
|
95
91
|
setHMem((h) => push(h, s.memPercent));
|
|
96
92
|
setHNet((h) => push(h, (s.netRecvBps + s.netSentBps) / 1024 / 1024));
|
|
@@ -102,11 +98,12 @@ export function App(props) {
|
|
|
102
98
|
const refreshAux = useCallback(async () => {
|
|
103
99
|
if (pausedRef.current || !tokRef.current)
|
|
104
100
|
return;
|
|
105
|
-
const [tk, vx, gt, zg] = await Promise.all([
|
|
101
|
+
const [tk, vx, gt, zg, hfd] = await Promise.all([
|
|
106
102
|
tokRef.current.read(),
|
|
107
|
-
|
|
103
|
+
readVtex(),
|
|
108
104
|
gitRef.current ? gitRef.current.read() : Promise.resolve(null),
|
|
109
105
|
zgitRef.current ? zgitRef.current.read() : Promise.resolve(null),
|
|
106
|
+
hfRef.current ? hfRef.current.read() : Promise.resolve(null),
|
|
110
107
|
]);
|
|
111
108
|
if (!mounted.current)
|
|
112
109
|
return;
|
|
@@ -114,7 +111,8 @@ export function App(props) {
|
|
|
114
111
|
setVtex(vx);
|
|
115
112
|
setGit(gt);
|
|
116
113
|
setZgit(zg);
|
|
117
|
-
|
|
114
|
+
setHf(hfd);
|
|
115
|
+
}, []);
|
|
118
116
|
useEffect(() => {
|
|
119
117
|
refreshData();
|
|
120
118
|
const id = setInterval(refreshData, Math.max(250, interval * 1000));
|
|
@@ -127,7 +125,6 @@ export function App(props) {
|
|
|
127
125
|
}, [refreshAux]);
|
|
128
126
|
useInput((input) => {
|
|
129
127
|
if (input === "q") {
|
|
130
|
-
powerRef.current?.stop();
|
|
131
128
|
exit();
|
|
132
129
|
}
|
|
133
130
|
else if (input === "p") {
|
|
@@ -148,6 +145,12 @@ export function App(props) {
|
|
|
148
145
|
const inner = (w) => Math.max(6, w - 4); // minus border (2) + padding (2)
|
|
149
146
|
const w2 = inner(col2);
|
|
150
147
|
const w3 = inner(col3);
|
|
148
|
+
// GIT + PROCESSES share a row on wide terminals; they stack when it's too
|
|
149
|
+
// narrow to split. GIT's columns are fixed-width, so it takes only what it
|
|
150
|
+
// needs (clamped) and PROCESSES inherits the slack — handy for long names.
|
|
151
|
+
const sideBySide = cols >= 92;
|
|
152
|
+
const gitW = Math.max(48, Math.min(58, Math.floor((cols - 2 * m) * 0.46)));
|
|
153
|
+
const procW = cols - 2 * m - gitW;
|
|
151
154
|
const statColor = (lvl) => lvl === "ok" ? pal.green : lvl === "warn" ? pal.yellow : pal.red;
|
|
152
155
|
const Stat = ({ value, warn, crit, metric }) => {
|
|
153
156
|
const lvl = statusLevel(value, warn, crit);
|
|
@@ -170,8 +173,12 @@ export function App(props) {
|
|
|
170
173
|
const util = gpu?.utilPct ?? 0;
|
|
171
174
|
const diskRows = disks?.disks ?? [];
|
|
172
175
|
const gitRepos = git?.repos ?? [];
|
|
176
|
+
const gitTotal = git?.total ?? 0;
|
|
173
177
|
const zgitServer = zgit?.available ? zgit : null;
|
|
174
|
-
const showGit =
|
|
178
|
+
const showGit = gitTotal > 0 || !!zgitServer;
|
|
179
|
+
const hfModels = hf?.models ?? [];
|
|
180
|
+
const showHf = !!hf?.available;
|
|
181
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
175
182
|
const hostLabel = (r) => r.hostKind === "github"
|
|
176
183
|
? "GitHub"
|
|
177
184
|
: r.hostKind === "zgit"
|
|
@@ -192,14 +199,9 @@ export function App(props) {
|
|
|
192
199
|
const modelExtra = modelItems.length > 2 ? ` +${modelItems.length - 2}` : "";
|
|
193
200
|
const web = tokens && (tokens.webSearch || tokens.webFetch) ? ` · web ${tokens.webSearch}/${tokens.webFetch}` : "";
|
|
194
201
|
const models = modelParts.length ? modelParts.join(" · ") + modelExtra + web : t("tokens.none");
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
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");
|
|
202
|
+
const tempLine2 = sensors?.fans?.length
|
|
203
|
+
? `${icon("fan")} ${sensors.fans[0].rpm.toFixed(0)} rpm`
|
|
204
|
+
: "";
|
|
203
205
|
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
206
|
used: humanBytes(sys?.memUsed),
|
|
205
207
|
total: humanBytes(sys?.memTotal),
|
|
@@ -207,18 +209,23 @@ export function App(props) {
|
|
|
207
209
|
}) }), _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
210
|
const lvl = statusLevel(d.usePercent, 80, 92);
|
|
209
211
|
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: "
|
|
212
|
+
}))] }), showHf && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.yellow, paddingX: 1, marginRight: 1, width: fullW, children: [_jsxs(Text, { color: pal.yellow, bold: true, children: [icon("hf"), " ", t("panel.hf")] }), _jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("hf.model").padEnd(30), t("hf.size").padEnd(9), t("hf.type").padEnd(16), t("hf.state")] }), hfModels.map((mdl) => (_jsxs(Text, { wrap: "truncate", children: [mdl.id.slice(0, 29).padEnd(30), humanBytes(mdl.sizeBytes).padEnd(9), (mdl.modelType || "—").slice(0, 15).padEnd(16), mdl.active ? (_jsxs(Text, { color: pal.green, children: ["\u25CF ", t("hf.active"), " ", mdl.procName.slice(0, 14), " ", mdl.pid] })) : mdl.lastUsed > 0 ? (_jsx(Text, { color: pal.comment, children: t("hf.usedAgo", { ago: agoShort(mdl.lastUsed, nowSec) }) })) : (_jsx(Text, { color: pal.comment, children: t("hf.idle") }))] }, mdl.id))), _jsx(Text, { color: pal.comment, wrap: "truncate", children: t("hf.summary", {
|
|
213
|
+
path: hf?.cachePath ?? "",
|
|
214
|
+
size: humanBytes(hf?.totalBytes),
|
|
215
|
+
n: hfModels.length,
|
|
216
|
+
active: hf?.activeCount ?? 0,
|
|
217
|
+
}) })] })), _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
218
|
total: fmtTok(tokTotal),
|
|
212
219
|
input: fmtTok(tokens?.input),
|
|
213
220
|
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 }) })] }),
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
}) }), _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) => {
|
|
222
|
+
const label = r.detached ? "(detached)" : r.branch;
|
|
223
|
+
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));
|
|
224
|
+
}), zgitServer && (_jsx(Text, { color: pal.comment, wrap: "truncate", children: zgitServer.repos.length
|
|
225
|
+
? t("git.server", {
|
|
226
|
+
container: zgitServer.container,
|
|
227
|
+
list: zgitServer.repos.join(", "),
|
|
228
|
+
n: zgitServer.repos.length,
|
|
229
|
+
})
|
|
230
|
+
: 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
231
|
}
|
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,
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
124
|
-
const variant =
|
|
125
|
-
const
|
|
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(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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, hfCachePath: cfg.hfCachePath }));
|
|
144
53
|
await waitUntilExit();
|
|
145
54
|
}
|
|
146
55
|
main().catch((e) => {
|
package/dist/collectors/git.js
CHANGED
|
@@ -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 =
|
|
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
|
|
50
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local HuggingFace hub cache — which models are installed, and which are live.
|
|
3
|
+
*
|
|
4
|
+
* The hub cache is the standard layout `<cache>/models--<org>--<name>/` with
|
|
5
|
+
* `refs/main` (the checked-out revision), `snapshots/<rev>/` (symlinks) and
|
|
6
|
+
* `blobs/<sha>` (the real file content). Enumerating models is pure filesystem
|
|
7
|
+
* work — no dependency on the `hf` CLI, so it keeps working under `npx`.
|
|
8
|
+
*
|
|
9
|
+
* Two usage signals are layered on top:
|
|
10
|
+
* - "active" — a process currently holds one of the model's files open (a
|
|
11
|
+
* single `lsof` pass; the file's path always contains the model's
|
|
12
|
+
* `models--org--name` segment because each repo has its own `blobs/`).
|
|
13
|
+
* - "lastUsed" — the atime of the model's largest blob (the weights), which
|
|
14
|
+
* reflects the last real read. `statSync` reads metadata only, so polling it
|
|
15
|
+
* never updates atime; the weights blob is used (never config.json) so our
|
|
16
|
+
* own one-time config read can't pollute the signal.
|
|
17
|
+
*
|
|
18
|
+
* Fail-soft like every collector: any error returns the previous snapshot.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
21
|
+
import { promisify } from "node:util";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
25
|
+
const run = promisify(execFile);
|
|
26
|
+
/** Resolve the hub cache dir: config → HF_HUB_CACHE → HF_HOME/hub → ~/.cache. */
|
|
27
|
+
function resolveCachePath(override) {
|
|
28
|
+
if (override)
|
|
29
|
+
return override;
|
|
30
|
+
if (process.env.HF_HUB_CACHE)
|
|
31
|
+
return process.env.HF_HUB_CACHE;
|
|
32
|
+
if (process.env.HF_HOME)
|
|
33
|
+
return join(process.env.HF_HOME, "hub");
|
|
34
|
+
return join(homedir(), ".cache", "huggingface", "hub");
|
|
35
|
+
}
|
|
36
|
+
export class HFCollector {
|
|
37
|
+
inflight = false;
|
|
38
|
+
last;
|
|
39
|
+
cachePath;
|
|
40
|
+
// model_type is static per revision and reading config.json bumps its atime,
|
|
41
|
+
// so we read it once and cache it keyed by "<dirName>@<revision>".
|
|
42
|
+
typeCache = new Map();
|
|
43
|
+
constructor(override) {
|
|
44
|
+
this.cachePath = resolveCachePath(override);
|
|
45
|
+
this.last = { available: false, cachePath: this.cachePath, totalBytes: 0, activeCount: 0, models: [] };
|
|
46
|
+
}
|
|
47
|
+
async read() {
|
|
48
|
+
if (this.inflight)
|
|
49
|
+
return this.last;
|
|
50
|
+
this.inflight = true;
|
|
51
|
+
try {
|
|
52
|
+
if (!existsSync(this.cachePath)) {
|
|
53
|
+
this.last = { available: false, cachePath: this.cachePath, totalBytes: 0, activeCount: 0, models: [] };
|
|
54
|
+
return this.last;
|
|
55
|
+
}
|
|
56
|
+
const dirs = readdirSync(this.cachePath, { withFileTypes: true })
|
|
57
|
+
.filter((e) => e.isDirectory() && e.name.startsWith("models--"))
|
|
58
|
+
.map((e) => e.name);
|
|
59
|
+
const byDir = new Map();
|
|
60
|
+
const models = [];
|
|
61
|
+
for (const dirName of dirs) {
|
|
62
|
+
const m = this.readModel(dirName);
|
|
63
|
+
if (m) {
|
|
64
|
+
models.push(m.model);
|
|
65
|
+
byDir.set(dirName, m.model);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// One lsof pass marks the models whose files a process currently holds.
|
|
69
|
+
await this.markActive(byDir);
|
|
70
|
+
const totalBytes = models.reduce((a, m) => a + m.sizeBytes, 0);
|
|
71
|
+
const activeCount = models.filter((m) => m.active).length;
|
|
72
|
+
models.sort((a, b) => Number(b.active) - Number(a.active) || b.lastUsed - a.lastUsed || b.sizeBytes - a.sizeBytes);
|
|
73
|
+
this.last = { available: models.length > 0, cachePath: this.cachePath, totalBytes, activeCount, models };
|
|
74
|
+
return this.last;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return this.last;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this.inflight = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Size (sum of blobs), weights atime, model_type and revision for one repo. */
|
|
84
|
+
readModel(dirName) {
|
|
85
|
+
try {
|
|
86
|
+
const id = dirName.replace(/^models--/, "").replace(/--/g, "/");
|
|
87
|
+
const name = id.slice(id.lastIndexOf("/") + 1);
|
|
88
|
+
const base = join(this.cachePath, dirName);
|
|
89
|
+
// Full hash locates the snapshot dir; a short prefix is what we display.
|
|
90
|
+
const revFull = readFileSync(join(base, "refs", "main"), "utf8").trim();
|
|
91
|
+
const revision = revFull.slice(0, 12);
|
|
92
|
+
// One pass over blobs: total size + atime of the largest blob (weights).
|
|
93
|
+
let sizeBytes = 0;
|
|
94
|
+
let biggest = -1;
|
|
95
|
+
let lastUsed = 0;
|
|
96
|
+
const blobsDir = join(base, "blobs");
|
|
97
|
+
if (existsSync(blobsDir)) {
|
|
98
|
+
for (const f of readdirSync(blobsDir)) {
|
|
99
|
+
try {
|
|
100
|
+
const st = statSync(join(blobsDir, f));
|
|
101
|
+
if (!st.isFile())
|
|
102
|
+
continue;
|
|
103
|
+
sizeBytes += st.size;
|
|
104
|
+
if (st.size > biggest) {
|
|
105
|
+
biggest = st.size;
|
|
106
|
+
lastUsed = Math.floor(st.atimeMs / 1000);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// skip unreadable blob
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const modelType = this.readType(dirName, revFull, base);
|
|
115
|
+
return {
|
|
116
|
+
model: { id, name, sizeBytes, modelType, revision, active: false, procName: "", pid: 0, lastUsed },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** config.json `model_type`, cached per revision (its read would bump atime). */
|
|
124
|
+
readType(dirName, revision, base) {
|
|
125
|
+
const key = `${dirName}@${revision}`;
|
|
126
|
+
const hit = this.typeCache.get(key);
|
|
127
|
+
if (hit !== undefined)
|
|
128
|
+
return hit;
|
|
129
|
+
let modelType = "";
|
|
130
|
+
try {
|
|
131
|
+
const cfg = JSON.parse(readFileSync(join(base, "snapshots", revision, "config.json"), "utf8"));
|
|
132
|
+
if (typeof cfg.model_type === "string")
|
|
133
|
+
modelType = cfg.model_type;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// no config.json (e.g. some MLX repos) — leave type blank
|
|
137
|
+
}
|
|
138
|
+
this.typeCache.set(key, modelType);
|
|
139
|
+
return modelType;
|
|
140
|
+
}
|
|
141
|
+
/** Single `lsof` pass; tag each model a process is holding files open for. */
|
|
142
|
+
async markActive(byDir) {
|
|
143
|
+
if (byDir.size === 0)
|
|
144
|
+
return;
|
|
145
|
+
// `lsof -nP` often exits non-zero with warnings about other users' procs
|
|
146
|
+
// while still printing useful stdout — keep the partial output from the error.
|
|
147
|
+
const { stdout } = await run("lsof", ["-nP", "-F", "pcn"], { maxBuffer: 1 << 24 }).catch((e) => ({ stdout: e?.stdout ?? "" }));
|
|
148
|
+
let pid = 0;
|
|
149
|
+
let cmd = "";
|
|
150
|
+
for (const line of stdout.split("\n")) {
|
|
151
|
+
const tag = line[0];
|
|
152
|
+
const val = line.slice(1);
|
|
153
|
+
if (tag === "p")
|
|
154
|
+
pid = Number(val) || 0;
|
|
155
|
+
else if (tag === "c")
|
|
156
|
+
cmd = val;
|
|
157
|
+
else if (tag === "n") {
|
|
158
|
+
const seg = val.match(/\/(models--[^/]+)\//);
|
|
159
|
+
if (!seg)
|
|
160
|
+
continue;
|
|
161
|
+
const model = byDir.get(seg[1]);
|
|
162
|
+
if (model && !model.active) {
|
|
163
|
+
model.active = true;
|
|
164
|
+
model.pid = pid;
|
|
165
|
+
model.procName = cmd;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -27,6 +27,8 @@ export function loadConfig() {
|
|
|
27
27
|
}
|
|
28
28
|
if (typeof raw.zgitContainer === "string" && raw.zgitContainer)
|
|
29
29
|
cfg.zgitContainer = raw.zgitContainer;
|
|
30
|
+
if (typeof raw.hfCachePath === "string" && raw.hfCachePath)
|
|
31
|
+
cfg.hfCachePath = raw.hfCachePath;
|
|
30
32
|
return cfg;
|
|
31
33
|
}
|
|
32
34
|
catch {
|
package/dist/format.js
CHANGED
|
@@ -45,6 +45,20 @@ export function sparkline(history, width = 0) {
|
|
|
45
45
|
})
|
|
46
46
|
.join("");
|
|
47
47
|
}
|
|
48
|
+
/** Short relative time ("now" / "3m" / "2h" / "5d") from a unix-seconds stamp. */
|
|
49
|
+
export function agoShort(unixSeconds, nowSeconds) {
|
|
50
|
+
const ts = Number(unixSeconds || 0);
|
|
51
|
+
if (!ts)
|
|
52
|
+
return "—";
|
|
53
|
+
const s = Math.max(0, Math.floor(nowSeconds - ts));
|
|
54
|
+
if (s < 60)
|
|
55
|
+
return "now";
|
|
56
|
+
if (s < 3600)
|
|
57
|
+
return `${Math.floor(s / 60)}m`;
|
|
58
|
+
if (s < 86400)
|
|
59
|
+
return `${Math.floor(s / 3600)}h`;
|
|
60
|
+
return `${Math.floor(s / 86400)}d`;
|
|
61
|
+
}
|
|
48
62
|
/** Pick a status level by thresholds (mirrors the Python status() helper). */
|
|
49
63
|
export function statusLevel(value, warn, crit) {
|
|
50
64
|
const v = Number(value || 0);
|
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 ·
|
|
3
|
+
"subtitle": "system · gpu · git",
|
|
4
4
|
"paused": "PAUSED",
|
|
5
5
|
"panel.cpu": "CPU",
|
|
6
6
|
"panel.mem": "MEMORY",
|
|
@@ -11,6 +11,7 @@ export const en = {
|
|
|
11
11
|
"panel.gpu": "GPU",
|
|
12
12
|
"panel.tokens": "AI · TOKENS TODAY",
|
|
13
13
|
"panel.vtex": "VTEX",
|
|
14
|
+
"panel.hf": "HUGGINGFACE",
|
|
14
15
|
"panel.procs": "PROCESSES",
|
|
15
16
|
"status.cpu.ok": "calm",
|
|
16
17
|
"status.cpu.warn": "busy",
|
|
@@ -40,16 +41,14 @@ export const en = {
|
|
|
40
41
|
"git.branch": "BRANCH",
|
|
41
42
|
"git.state": "STATE",
|
|
42
43
|
"git.host": "HOST",
|
|
43
|
-
"git.
|
|
44
|
-
"git.
|
|
44
|
+
"git.uncommitted": "uncommitted",
|
|
45
|
+
"git.allClear": "✓ all {n} repos up to date",
|
|
45
46
|
"git.local": "local",
|
|
46
47
|
"git.server": "— zgit server ({container}): {list} ({n})",
|
|
47
48
|
"git.serverEmpty": "— zgit server ({container}): no repos",
|
|
48
49
|
"gpu.usage": "Usage",
|
|
49
50
|
"gpu.mem": "mem {used}/{alloc}",
|
|
50
51
|
"temp.unavailable": "sensors unavailable",
|
|
51
|
-
"temp.waitingSudo": "waiting for sudo",
|
|
52
|
-
"temp.needsSudo": "power: requires sudo",
|
|
53
52
|
"spark.collecting": "collecting…",
|
|
54
53
|
"spark.lastMin": "last min: {range}",
|
|
55
54
|
"tokens.spent": "Spent today: ${cost} · {messages} messages",
|
|
@@ -66,6 +65,15 @@ export const en = {
|
|
|
66
65
|
"vtex.workspace": "Workspace",
|
|
67
66
|
"vtex.notConnected": "not connected",
|
|
68
67
|
"vtex.signin": "Sign in with: vtex login <account>",
|
|
68
|
+
"hf.model": "MODEL",
|
|
69
|
+
"hf.size": "SIZE",
|
|
70
|
+
"hf.type": "TYPE",
|
|
71
|
+
"hf.state": "STATE",
|
|
72
|
+
"hf.active": "active",
|
|
73
|
+
"hf.idle": "idle",
|
|
74
|
+
"hf.usedAgo": "used {ago} ago",
|
|
75
|
+
"hf.empty": "no models cached",
|
|
76
|
+
"hf.summary": "{path} · {size} · {n} models · {active} active",
|
|
69
77
|
"proc.cpu": "CPU%",
|
|
70
78
|
"proc.mem": "MEM",
|
|
71
79
|
"proc.pid": "PID",
|
|
@@ -74,7 +82,4 @@ export const en = {
|
|
|
74
82
|
"key.pause": "Pause",
|
|
75
83
|
"key.faster": "Faster",
|
|
76
84
|
"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
85
|
};
|
package/dist/i18n/es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const es = {
|
|
2
|
-
"subtitle": "sistema · gpu ·
|
|
2
|
+
"subtitle": "sistema · gpu · git",
|
|
3
3
|
"paused": "PAUSADO",
|
|
4
4
|
"panel.cpu": "CPU",
|
|
5
5
|
"panel.mem": "MEMORIA",
|
|
@@ -10,6 +10,7 @@ export const es = {
|
|
|
10
10
|
"panel.gpu": "GPU",
|
|
11
11
|
"panel.tokens": "IA · TOKENS HOY",
|
|
12
12
|
"panel.vtex": "VTEX",
|
|
13
|
+
"panel.hf": "HUGGINGFACE",
|
|
13
14
|
"panel.procs": "PROCESOS",
|
|
14
15
|
"status.cpu.ok": "tranquilo",
|
|
15
16
|
"status.cpu.warn": "ocupado",
|
|
@@ -39,16 +40,14 @@ export const es = {
|
|
|
39
40
|
"git.branch": "BRANCH",
|
|
40
41
|
"git.state": "ESTADO",
|
|
41
42
|
"git.host": "HOST",
|
|
42
|
-
"git.
|
|
43
|
-
"git.
|
|
43
|
+
"git.uncommitted": "sin commit",
|
|
44
|
+
"git.allClear": "✓ {n} repos al día",
|
|
44
45
|
"git.local": "local",
|
|
45
46
|
"git.server": "— servidor zgit ({container}): {list} ({n})",
|
|
46
47
|
"git.serverEmpty": "— servidor zgit ({container}): sin repos",
|
|
47
48
|
"gpu.usage": "Uso",
|
|
48
49
|
"gpu.mem": "mem {used}/{alloc}",
|
|
49
50
|
"temp.unavailable": "sensores no disponibles",
|
|
50
|
-
"temp.waitingSudo": "esperando sudo",
|
|
51
|
-
"temp.needsSudo": "energía: requiere sudo",
|
|
52
51
|
"spark.collecting": "recolectando…",
|
|
53
52
|
"spark.lastMin": "último min: {range}",
|
|
54
53
|
"tokens.spent": "Gastado hoy: ${cost} · {messages} mensajes",
|
|
@@ -65,6 +64,15 @@ export const es = {
|
|
|
65
64
|
"vtex.workspace": "Workspace",
|
|
66
65
|
"vtex.notConnected": "no conectado",
|
|
67
66
|
"vtex.signin": "Entra con: vtex login <cuenta>",
|
|
67
|
+
"hf.model": "MODELO",
|
|
68
|
+
"hf.size": "TAMAÑO",
|
|
69
|
+
"hf.type": "TIPO",
|
|
70
|
+
"hf.state": "ESTADO",
|
|
71
|
+
"hf.active": "activo",
|
|
72
|
+
"hf.idle": "inactivo",
|
|
73
|
+
"hf.usedAgo": "usado hace {ago}",
|
|
74
|
+
"hf.empty": "sin modelos en caché",
|
|
75
|
+
"hf.summary": "{path} · {size} · {n} modelos · {active} activos",
|
|
68
76
|
"proc.cpu": "CPU%",
|
|
69
77
|
"proc.mem": "MEM",
|
|
70
78
|
"proc.pid": "PID",
|
|
@@ -73,7 +81,4 @@ export const es = {
|
|
|
73
81
|
"key.pause": "Pausar",
|
|
74
82
|
"key.faster": "Más rápido",
|
|
75
83
|
"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
84
|
};
|
package/dist/i18n/pt-BR.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const ptBR = {
|
|
2
|
-
"subtitle": "sistema · gpu ·
|
|
2
|
+
"subtitle": "sistema · gpu · git",
|
|
3
3
|
"paused": "PAUSADO",
|
|
4
4
|
"panel.cpu": "CPU",
|
|
5
5
|
"panel.mem": "MEMÓRIA",
|
|
@@ -10,6 +10,7 @@ export const ptBR = {
|
|
|
10
10
|
"panel.gpu": "GPU",
|
|
11
11
|
"panel.tokens": "IA · TOKENS HOJE",
|
|
12
12
|
"panel.vtex": "VTEX",
|
|
13
|
+
"panel.hf": "HUGGINGFACE",
|
|
13
14
|
"panel.procs": "PROCESSOS",
|
|
14
15
|
"status.cpu.ok": "tranquilo",
|
|
15
16
|
"status.cpu.warn": "ocupado",
|
|
@@ -39,16 +40,14 @@ export const ptBR = {
|
|
|
39
40
|
"git.branch": "BRANCH",
|
|
40
41
|
"git.state": "ESTADO",
|
|
41
42
|
"git.host": "HOST",
|
|
42
|
-
"git.
|
|
43
|
-
"git.
|
|
43
|
+
"git.uncommitted": "sem commit",
|
|
44
|
+
"git.allClear": "✓ todos os {n} repos em dia",
|
|
44
45
|
"git.local": "local",
|
|
45
46
|
"git.server": "— servidor zgit ({container}): {list} ({n})",
|
|
46
47
|
"git.serverEmpty": "— servidor zgit ({container}): sem repos",
|
|
47
48
|
"gpu.usage": "Uso",
|
|
48
49
|
"gpu.mem": "mem {used}/{alloc}",
|
|
49
50
|
"temp.unavailable": "sensores indisponíveis",
|
|
50
|
-
"temp.waitingSudo": "aguardando sudo",
|
|
51
|
-
"temp.needsSudo": "energia: requer sudo",
|
|
52
51
|
"spark.collecting": "coletando…",
|
|
53
52
|
"spark.lastMin": "último min: {range}",
|
|
54
53
|
"tokens.spent": "Gasto hoje: ${cost} · {messages} mensagens",
|
|
@@ -65,6 +64,15 @@ export const ptBR = {
|
|
|
65
64
|
"vtex.workspace": "Workspace",
|
|
66
65
|
"vtex.notConnected": "não conectado",
|
|
67
66
|
"vtex.signin": "Entre com: vtex login <conta>",
|
|
67
|
+
"hf.model": "MODELO",
|
|
68
|
+
"hf.size": "TAMANHO",
|
|
69
|
+
"hf.type": "TIPO",
|
|
70
|
+
"hf.state": "ESTADO",
|
|
71
|
+
"hf.active": "ativo",
|
|
72
|
+
"hf.idle": "ocioso",
|
|
73
|
+
"hf.usedAgo": "usado há {ago}",
|
|
74
|
+
"hf.empty": "nenhum modelo em cache",
|
|
75
|
+
"hf.summary": "{path} · {size} · {n} modelos · {active} ativos",
|
|
68
76
|
"proc.cpu": "CPU%",
|
|
69
77
|
"proc.mem": "MEM",
|
|
70
78
|
"proc.pid": "PID",
|
|
@@ -73,7 +81,4 @@ export const ptBR = {
|
|
|
73
81
|
"key.pause": "Pausar",
|
|
74
82
|
"key.faster": "Mais rápido",
|
|
75
83
|
"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
84
|
};
|
package/dist/icons.js
CHANGED
|
@@ -19,6 +19,7 @@ const NERD = {
|
|
|
19
19
|
tokens: "", // money
|
|
20
20
|
vtex: "", // shopping-cart
|
|
21
21
|
git: "\uF126", // code-fork
|
|
22
|
+
hf: "\uF1B3", // cubes (model artifacts)
|
|
22
23
|
proc: "", // tasks
|
|
23
24
|
clock: "", // clock
|
|
24
25
|
};
|
|
@@ -36,6 +37,7 @@ const EMOJI = {
|
|
|
36
37
|
tokens: "🪙",
|
|
37
38
|
vtex: "🛒",
|
|
38
39
|
git: "🌿",
|
|
40
|
+
hf: "🤗",
|
|
39
41
|
proc: "📋",
|
|
40
42
|
clock: "🕐",
|
|
41
43
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeluizr/lattice",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.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
|
-
"
|
|
37
|
+
"git",
|
|
38
38
|
"htop",
|
|
39
39
|
"ink",
|
|
40
40
|
"cli"
|
package/dist/collectors/power.js
DELETED
|
@@ -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
|
-
}
|