@zeluizr/lattice 1.0.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 +35 -0
- package/README.md +12 -1
- package/dist/app.js +52 -31
- package/dist/cli.js +24 -82
- package/dist/collectors/git.js +85 -24
- package/dist/collectors/zgit.js +42 -0
- package/dist/config.js +7 -0
- package/dist/i18n/en.js +7 -8
- package/dist/i18n/es.js +7 -8
- package/dist/i18n/pt-BR.js +7 -8
- package/package.json +5 -2
- package/dist/collectors/power.js +0 -105
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ 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
|
+
## [2.0.0] — 2026-06-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **GIT panel reports the host of each repo.** A new `HOST` column tags every
|
|
11
|
+
repo by where its `origin` lives — **GitHub**, the self-hosted **zgit** server,
|
|
12
|
+
another host, or `local` (no remote) — parsed from the remote URL.
|
|
13
|
+
- **zgit server listing.** A footer line in the GIT panel lists the bare repos on
|
|
14
|
+
the self-hosted zgit Docker container (`docker exec -u git <container> ls
|
|
15
|
+
/repos`). Local-only, no network; fails soft (the line is hidden) when Docker
|
|
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.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
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
|
|
25
|
+
folder whose subdirectories are repos; entries are merged and de-duplicated.
|
|
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`.
|
|
41
|
+
|
|
7
42
|
## [1.0.0] — 2026-06-24
|
|
8
43
|
|
|
9
44
|
First open-source release as **lattice** — a full rewrite of the project
|
package/README.md
CHANGED
|
@@ -21,7 +21,14 @@ your Mac.
|
|
|
21
21
|
|
|
22
22
|
## Install & run
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
`@zeluizr/lattice` is a **private** npm package. Log in to an npm account that
|
|
25
|
+
has access to it first:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm login
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then run it straight from npm:
|
|
25
32
|
|
|
26
33
|
```bash
|
|
27
34
|
npx @zeluizr/lattice
|
|
@@ -34,6 +41,10 @@ npm i -g @zeluizr/lattice
|
|
|
34
41
|
lattice
|
|
35
42
|
```
|
|
36
43
|
|
|
44
|
+
> **Access:** the package is published with `access: restricted`. The owner
|
|
45
|
+
> grants teammates access with `npm access grant read-only <user> @zeluizr/lattice`
|
|
46
|
+
> (or by publishing under an npm **org** scope and adding them to a team).
|
|
47
|
+
|
|
37
48
|
On first run, lattice asks for your language (English · Español · Português) and
|
|
38
49
|
remembers it. Watts need `sudo` (see below); everything else works without it.
|
|
39
50
|
|
package/dist/app.js
CHANGED
|
@@ -5,27 +5,29 @@ import { Panel } from "./components/Panel.js";
|
|
|
5
5
|
import { SystemCollector } from "./collectors/system.js";
|
|
6
6
|
import { DisksCollector } from "./collectors/disks.js";
|
|
7
7
|
import { GitCollector } from "./collectors/git.js";
|
|
8
|
+
import { ZgitCollector } from "./collectors/zgit.js";
|
|
8
9
|
import { readGpu } from "./collectors/gpu.js";
|
|
9
10
|
import { readSensors } from "./collectors/sensors.js";
|
|
10
|
-
import { PowerCollector } from "./collectors/power.js";
|
|
11
11
|
import { TokenCollector } from "./collectors/tokens.js";
|
|
12
12
|
import { readVtex } from "./collectors/vtex.js";
|
|
13
13
|
import { coreCell, fmtTok, humanBytes, humanRate, sparkline, statusLevel } from "./format.js";
|
|
14
14
|
const MAX_HIST = 60;
|
|
15
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
|
|
16
18
|
function timeStr() {
|
|
17
19
|
const d = new Date();
|
|
18
20
|
return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
19
21
|
}
|
|
20
22
|
export function App(props) {
|
|
21
|
-
const { t, pal, icon,
|
|
23
|
+
const { t, pal, icon, repoRoots, zgitContainer } = props;
|
|
22
24
|
const { exit } = useApp();
|
|
23
25
|
const [sys, setSys] = useState(null);
|
|
24
26
|
const [disks, setDisks] = useState(null);
|
|
25
27
|
const [git, setGit] = useState(null);
|
|
28
|
+
const [zgit, setZgit] = useState(null);
|
|
26
29
|
const [gpu, setGpu] = useState(null);
|
|
27
30
|
const [sensors, setSensors] = useState(null);
|
|
28
|
-
const [power, setPower] = useState(null);
|
|
29
31
|
const [tokens, setTokens] = useState(null);
|
|
30
32
|
const [vtex, setVtex] = useState(null);
|
|
31
33
|
const [hCpu, setHCpu] = useState([]);
|
|
@@ -34,14 +36,14 @@ export function App(props) {
|
|
|
34
36
|
const [hGpu, setHGpu] = useState([]);
|
|
35
37
|
const [hTemp, setHTemp] = useState([]);
|
|
36
38
|
const [paused, setPaused] = useState(false);
|
|
37
|
-
const [interval, setIntervalState] = useState(
|
|
39
|
+
const [interval, setIntervalState] = useState(DEFAULT_INTERVAL);
|
|
38
40
|
const [now, setNow] = useState(timeStr());
|
|
39
41
|
const [cols, setCols] = useState(process.stdout.columns || 100);
|
|
40
42
|
const sysRef = useRef(null);
|
|
41
43
|
const disksRef = useRef(null);
|
|
42
44
|
const gitRef = useRef(null);
|
|
45
|
+
const zgitRef = useRef(null);
|
|
43
46
|
const tokRef = useRef(null);
|
|
44
|
-
const powerRef = useRef(null);
|
|
45
47
|
const pausedRef = useRef(false);
|
|
46
48
|
const mounted = useRef(true);
|
|
47
49
|
useEffect(() => {
|
|
@@ -50,14 +52,11 @@ export function App(props) {
|
|
|
50
52
|
// ----- init collectors + power subprocess + clock -------------------------
|
|
51
53
|
useEffect(() => {
|
|
52
54
|
mounted.current = true;
|
|
53
|
-
sysRef.current = new SystemCollector(
|
|
55
|
+
sysRef.current = new SystemCollector(PROCS);
|
|
54
56
|
disksRef.current = new DisksCollector();
|
|
55
|
-
gitRef.current = new GitCollector(
|
|
57
|
+
gitRef.current = new GitCollector(repoRoots);
|
|
58
|
+
zgitRef.current = new ZgitCollector(zgitContainer);
|
|
56
59
|
tokRef.current = new TokenCollector();
|
|
57
|
-
if (usePower) {
|
|
58
|
-
powerRef.current = new PowerCollector(Math.round(props.interval * 1000));
|
|
59
|
-
powerRef.current.start();
|
|
60
|
-
}
|
|
61
60
|
const clock = setInterval(() => mounted.current && setNow(timeStr()), 1000);
|
|
62
61
|
const onResize = () => setCols(process.stdout.columns || 100);
|
|
63
62
|
process.stdout.on("resize", onResize);
|
|
@@ -65,7 +64,6 @@ export function App(props) {
|
|
|
65
64
|
mounted.current = false;
|
|
66
65
|
clearInterval(clock);
|
|
67
66
|
process.stdout.off("resize", onResize);
|
|
68
|
-
powerRef.current?.stop();
|
|
69
67
|
};
|
|
70
68
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
69
|
}, []);
|
|
@@ -79,14 +77,12 @@ export function App(props) {
|
|
|
79
77
|
readGpu(),
|
|
80
78
|
readSensors(),
|
|
81
79
|
]);
|
|
82
|
-
const p = powerRef.current ? powerRef.current.read() : null;
|
|
83
80
|
if (!mounted.current)
|
|
84
81
|
return;
|
|
85
82
|
setSys(s);
|
|
86
83
|
setDisks(d);
|
|
87
84
|
setGpu(g);
|
|
88
85
|
setSensors(st);
|
|
89
|
-
setPower(p);
|
|
90
86
|
setHCpu((h) => push(h, s.cpuTotal));
|
|
91
87
|
setHMem((h) => push(h, s.memPercent));
|
|
92
88
|
setHNet((h) => push(h, (s.netRecvBps + s.netSentBps) / 1024 / 1024));
|
|
@@ -98,17 +94,19 @@ export function App(props) {
|
|
|
98
94
|
const refreshAux = useCallback(async () => {
|
|
99
95
|
if (pausedRef.current || !tokRef.current)
|
|
100
96
|
return;
|
|
101
|
-
const [tk, vx, gt] = await Promise.all([
|
|
97
|
+
const [tk, vx, gt, zg] = await Promise.all([
|
|
102
98
|
tokRef.current.read(),
|
|
103
|
-
|
|
99
|
+
readVtex(),
|
|
104
100
|
gitRef.current ? gitRef.current.read() : Promise.resolve(null),
|
|
101
|
+
zgitRef.current ? zgitRef.current.read() : Promise.resolve(null),
|
|
105
102
|
]);
|
|
106
103
|
if (!mounted.current)
|
|
107
104
|
return;
|
|
108
105
|
setTokens(tk);
|
|
109
106
|
setVtex(vx);
|
|
110
107
|
setGit(gt);
|
|
111
|
-
|
|
108
|
+
setZgit(zg);
|
|
109
|
+
}, []);
|
|
112
110
|
useEffect(() => {
|
|
113
111
|
refreshData();
|
|
114
112
|
const id = setInterval(refreshData, Math.max(250, interval * 1000));
|
|
@@ -121,7 +119,6 @@ export function App(props) {
|
|
|
121
119
|
}, [refreshAux]);
|
|
122
120
|
useInput((input) => {
|
|
123
121
|
if (input === "q") {
|
|
124
|
-
powerRef.current?.stop();
|
|
125
122
|
exit();
|
|
126
123
|
}
|
|
127
124
|
else if (input === "p") {
|
|
@@ -142,6 +139,12 @@ export function App(props) {
|
|
|
142
139
|
const inner = (w) => Math.max(6, w - 4); // minus border (2) + padding (2)
|
|
143
140
|
const w2 = inner(col2);
|
|
144
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;
|
|
145
148
|
const statColor = (lvl) => lvl === "ok" ? pal.green : lvl === "warn" ? pal.yellow : pal.red;
|
|
146
149
|
const Stat = ({ value, warn, crit, metric }) => {
|
|
147
150
|
const lvl = statusLevel(value, warn, crit);
|
|
@@ -164,20 +167,32 @@ export function App(props) {
|
|
|
164
167
|
const util = gpu?.utilPct ?? 0;
|
|
165
168
|
const diskRows = disks?.disks ?? [];
|
|
166
169
|
const gitRepos = git?.repos ?? [];
|
|
170
|
+
const gitTotal = git?.total ?? 0;
|
|
171
|
+
const zgitServer = zgit?.available ? zgit : null;
|
|
172
|
+
const showGit = gitTotal > 0 || !!zgitServer;
|
|
173
|
+
const hostLabel = (r) => r.hostKind === "github"
|
|
174
|
+
? "GitHub"
|
|
175
|
+
: r.hostKind === "zgit"
|
|
176
|
+
? "zgit"
|
|
177
|
+
: r.hostKind === "none"
|
|
178
|
+
? t("git.local")
|
|
179
|
+
: r.host || "—";
|
|
180
|
+
const hostColor = (r) => r.hostKind === "github"
|
|
181
|
+
? pal.purple
|
|
182
|
+
: r.hostKind === "zgit"
|
|
183
|
+
? pal.orange
|
|
184
|
+
: r.hostKind === "none"
|
|
185
|
+
? pal.comment
|
|
186
|
+
: pal.cyan;
|
|
167
187
|
const tokTotal = tokens ? tokens.input + tokens.output + tokens.cacheW + tokens.cacheR : 0;
|
|
168
188
|
const modelItems = Object.entries(tokens?.byModel ?? {}).sort((a, b) => b[1].cost - a[1].cost);
|
|
169
189
|
const modelParts = modelItems.slice(0, 2).map(([k, v]) => `${k} $${v.cost.toFixed(2)}`);
|
|
170
190
|
const modelExtra = modelItems.length > 2 ? ` +${modelItems.length - 2}` : "";
|
|
171
191
|
const web = tokens && (tokens.webSearch || tokens.webFetch) ? ` · web ${tokens.webSearch}/${tokens.webFetch}` : "";
|
|
172
192
|
const models = modelParts.length ? modelParts.join(" · ") + modelExtra + web : t("tokens.none");
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
else if (usePower)
|
|
177
|
-
powerParts.push(`${icon("power")} ${t("temp.waitingSudo")}`);
|
|
178
|
-
if (sensors?.fans?.length)
|
|
179
|
-
powerParts.push(`${icon("fan")} ${sensors.fans[0].rpm.toFixed(0)} rpm`);
|
|
180
|
-
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
|
+
: "";
|
|
181
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", {
|
|
182
197
|
used: humanBytes(sys?.memUsed),
|
|
183
198
|
total: humanBytes(sys?.memTotal),
|
|
@@ -185,12 +200,18 @@ export function App(props) {
|
|
|
185
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) => {
|
|
186
201
|
const lvl = statusLevel(d.usePercent, 80, 92);
|
|
187
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));
|
|
188
|
-
}))] }), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Panel, { title: `${icon("tokens")} ${t("panel.tokens")}`, color: pal.pink, width:
|
|
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", {
|
|
189
204
|
total: fmtTok(tokTotal),
|
|
190
205
|
input: fmtTok(tokens?.input),
|
|
191
206
|
output: fmtTok(tokens?.output),
|
|
192
|
-
}) }), _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 }) })] }),
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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"] }) })] }));
|
|
196
217
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,66 +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 folder of git repos to show branches for
|
|
23
|
-
(default: the parent of the current directory)
|
|
24
|
-
--interval, -i refresh interval in seconds (default 1)
|
|
25
|
-
--procs, -n number of top processes to show (default 8)
|
|
26
|
-
--icons icon style: nerd | emoji | none (default nerd)
|
|
27
|
-
--lang language: en | es | pt-BR (asked on first run)
|
|
28
|
-
--theme pro | blade | buffy | lincoln | morbius | van-helsing
|
|
29
|
-
--version, -v
|
|
30
|
-
--help
|
|
31
|
-
|
|
32
|
-
Examples
|
|
33
19
|
$ lattice
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
interval: { type: "number", shortFlag: "i", default: 1 },
|
|
44
|
-
procs: { type: "number", shortFlag: "n", default: 8 },
|
|
45
|
-
icons: { type: "string", default: "" },
|
|
46
|
-
lang: { type: "string", default: "" },
|
|
47
|
-
theme: { type: "string", default: "" },
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
async function resolveLang() {
|
|
51
|
-
const cfg = loadConfig();
|
|
52
|
-
const flag = cli.flags.lang;
|
|
53
|
-
if (flag && isLang(flag)) {
|
|
54
|
-
saveConfig({ lang: flag });
|
|
55
|
-
return flag;
|
|
56
|
-
}
|
|
57
|
-
if (cfg.lang)
|
|
58
|
-
return cfg.lang;
|
|
59
|
-
// 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;
|
|
60
29
|
if (!process.stdin.isTTY) {
|
|
61
30
|
const detected = detectLang();
|
|
62
31
|
saveConfig({ lang: detected });
|
|
63
32
|
return detected;
|
|
64
33
|
}
|
|
65
|
-
const variant = resolveTheme();
|
|
66
34
|
const chosen = await new Promise((resolve) => {
|
|
67
35
|
const app = render(_jsx(LanguageSelect, { pal: palette(variant), onSelect: (l) => {
|
|
68
36
|
app.unmount();
|
|
@@ -72,42 +40,16 @@ async function resolveLang() {
|
|
|
72
40
|
saveConfig({ lang: chosen });
|
|
73
41
|
return chosen;
|
|
74
42
|
}
|
|
75
|
-
function resolveTheme() {
|
|
76
|
-
const flag = cli.flags.theme;
|
|
77
|
-
if (flag && isVariant(flag)) {
|
|
78
|
-
saveConfig({ theme: flag });
|
|
79
|
-
return flag;
|
|
80
|
-
}
|
|
81
|
-
return loadConfig().theme ?? "pro";
|
|
82
|
-
}
|
|
83
|
-
function resolveIcons() {
|
|
84
|
-
const flag = cli.flags.icons;
|
|
85
|
-
if (flag && isIconMode(flag)) {
|
|
86
|
-
saveConfig({ icons: flag });
|
|
87
|
-
return flag;
|
|
88
|
-
}
|
|
89
|
-
return loadConfig().icons ?? "nerd";
|
|
90
|
-
}
|
|
91
43
|
async function main() {
|
|
92
|
-
const
|
|
93
|
-
const variant =
|
|
94
|
-
const
|
|
44
|
+
const cfg = loadConfig();
|
|
45
|
+
const variant = cfg.theme ?? "pro";
|
|
46
|
+
const lang = await resolveLang(variant);
|
|
95
47
|
const t = makeT(lang);
|
|
96
48
|
const pal = palette(variant);
|
|
97
|
-
const icon = makeIcons(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
process.stdout.write(t("cli.sudoNeed") + "\n");
|
|
102
|
-
try {
|
|
103
|
-
await execa("sudo", ["-v"], { stdio: "inherit" });
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
process.stdout.write(t("cli.sudoFail") + "\n");
|
|
107
|
-
usePower = false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const { waitUntilExit } = render(_jsx(App, { t: t, pal: pal, icon: icon, lang: lang, usePower: usePower, useVtex: cli.flags.vtex, gitDir: cli.flags.repos || dirname(process.cwd()), 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 }));
|
|
111
53
|
await waitUntilExit();
|
|
112
54
|
}
|
|
113
55
|
main().catch((e) => {
|
package/dist/collectors/git.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Active git branches for
|
|
2
|
+
* Active git branches for a fixed, configured set of repos (no sudo).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Each configured root is either a repo itself (it has a `.git`) or a folder
|
|
5
|
+
* whose immediate subdirectories are repos. Both shapes are merged and
|
|
6
|
+
* de-duplicated by absolute path, so the report is the SAME no matter which
|
|
7
|
+
* directory lattice was launched from — it does not depend on the cwd.
|
|
8
|
+
*
|
|
9
|
+
* Each repo reports its branch, dirty/ahead/behind state (from a single
|
|
10
|
+
* `git status --porcelain=v2 --branch`) and where its `origin` is hosted —
|
|
11
|
+
* GitHub vs the self-hosted zgit server vs another host — from its remote URL.
|
|
8
12
|
*
|
|
9
13
|
* Cheap to discover (one readdir + a stat per child) but each repo costs a git
|
|
10
14
|
* invocation, so we cap the count, bound concurrency, and skip overlapping
|
|
@@ -16,28 +20,23 @@ import { readdir } from "node:fs/promises";
|
|
|
16
20
|
import { existsSync } from "node:fs";
|
|
17
21
|
import { join, basename } from "node:path";
|
|
18
22
|
const run = promisify(execFile);
|
|
19
|
-
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
|
|
20
25
|
const CONCURRENCY = 8;
|
|
21
26
|
export class GitCollector {
|
|
22
|
-
|
|
27
|
+
roots;
|
|
23
28
|
inflight = false;
|
|
24
29
|
last;
|
|
25
|
-
constructor(
|
|
26
|
-
this.
|
|
27
|
-
this.last = {
|
|
30
|
+
constructor(roots) {
|
|
31
|
+
this.roots = roots;
|
|
32
|
+
this.last = { roots, repos: [], total: 0, truncated: false };
|
|
28
33
|
}
|
|
29
34
|
async read() {
|
|
30
35
|
if (this.inflight)
|
|
31
36
|
return this.last;
|
|
32
37
|
this.inflight = true;
|
|
33
38
|
try {
|
|
34
|
-
const
|
|
35
|
-
const dirs = entries
|
|
36
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
37
|
-
.map((e) => join(this.root, e.name))
|
|
38
|
-
.filter((p) => existsSync(join(p, ".git")))
|
|
39
|
-
.sort();
|
|
40
|
-
const truncated = dirs.length > MAX_REPOS;
|
|
39
|
+
const dirs = await this.discover();
|
|
41
40
|
const pick = dirs.slice(0, MAX_REPOS);
|
|
42
41
|
const repos = [];
|
|
43
42
|
for (let i = 0; i < pick.length; i += CONCURRENCY) {
|
|
@@ -47,8 +46,19 @@ export class GitCollector {
|
|
|
47
46
|
if (r)
|
|
48
47
|
repos.push(r);
|
|
49
48
|
}
|
|
50
|
-
repos
|
|
51
|
-
|
|
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
|
+
};
|
|
52
62
|
return this.last;
|
|
53
63
|
}
|
|
54
64
|
catch {
|
|
@@ -58,17 +68,51 @@ export class GitCollector {
|
|
|
58
68
|
this.inflight = false;
|
|
59
69
|
}
|
|
60
70
|
}
|
|
71
|
+
/** Resolve every configured root to a de-duplicated, sorted list of repo dirs. */
|
|
72
|
+
async discover() {
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const out = [];
|
|
75
|
+
const add = (p) => {
|
|
76
|
+
if (!seen.has(p)) {
|
|
77
|
+
seen.add(p);
|
|
78
|
+
out.push(p);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
for (const root of this.roots) {
|
|
82
|
+
// A configured path can be a repo itself…
|
|
83
|
+
if (existsSync(join(root, ".git"))) {
|
|
84
|
+
add(root);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// …or a folder whose immediate children are repos.
|
|
88
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
89
|
+
for (const e of entries) {
|
|
90
|
+
if (!e.isDirectory() || e.name.startsWith("."))
|
|
91
|
+
continue;
|
|
92
|
+
const p = join(root, e.name);
|
|
93
|
+
if (existsSync(join(p, ".git")))
|
|
94
|
+
add(p);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out.sort();
|
|
98
|
+
}
|
|
61
99
|
}
|
|
62
100
|
async function readRepo(dir) {
|
|
63
|
-
const
|
|
64
|
-
maxBuffer: 1 << 20,
|
|
65
|
-
|
|
101
|
+
const [statusRes, urlRes, dateRes] = await Promise.all([
|
|
102
|
+
run("git", ["-C", dir, "status", "--porcelain=v2", "--branch"], { maxBuffer: 1 << 20 }),
|
|
103
|
+
run("git", ["-C", dir, "remote", "get-url", "origin"], { maxBuffer: 1 << 16 }).catch(() => ({
|
|
104
|
+
stdout: "",
|
|
105
|
+
})),
|
|
106
|
+
run("git", ["-C", dir, "log", "-1", "--format=%ct"], { maxBuffer: 1 << 16 }).catch(() => ({
|
|
107
|
+
stdout: "",
|
|
108
|
+
})),
|
|
109
|
+
]);
|
|
66
110
|
let branch = "?";
|
|
67
111
|
let ahead = 0;
|
|
68
112
|
let behind = 0;
|
|
69
113
|
let detached = false;
|
|
70
114
|
let dirty = false;
|
|
71
|
-
for (const ln of stdout.split("\n")) {
|
|
115
|
+
for (const ln of statusRes.stdout.split("\n")) {
|
|
72
116
|
if (ln.startsWith("# branch.head ")) {
|
|
73
117
|
branch = ln.slice("# branch.head ".length).trim();
|
|
74
118
|
if (branch === "(detached)")
|
|
@@ -85,5 +129,22 @@ async function readRepo(dir) {
|
|
|
85
129
|
dirty = true;
|
|
86
130
|
}
|
|
87
131
|
}
|
|
88
|
-
|
|
132
|
+
const { host, hostKind } = classifyHost(urlRes.stdout.trim());
|
|
133
|
+
const lastCommit = Number(dateRes.stdout.trim()) || 0;
|
|
134
|
+
return { name: basename(dir), path: dir, branch, ahead, behind, detached, dirty, host, hostKind, lastCommit };
|
|
135
|
+
}
|
|
136
|
+
/** Pull the hostname out of an origin URL and bucket it into a known host. */
|
|
137
|
+
export function classifyHost(url) {
|
|
138
|
+
if (!url)
|
|
139
|
+
return { host: "", hostKind: "none" };
|
|
140
|
+
// https://host/…, ssh://git@host/… or scp-like git@host:path
|
|
141
|
+
const scheme = url.match(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i);
|
|
142
|
+
const scp = url.match(/^[^/@]+@([^:/]+):/);
|
|
143
|
+
const host = (scheme?.[1] || scp?.[1] || "").toLowerCase();
|
|
144
|
+
let hostKind = "other";
|
|
145
|
+
if (host.endsWith("github.com"))
|
|
146
|
+
hostKind = "github";
|
|
147
|
+
else if (host.includes("zgit"))
|
|
148
|
+
hostKind = "zgit";
|
|
149
|
+
return { host, hostKind };
|
|
89
150
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bare repos living on the self-hosted zgit server (a Docker container).
|
|
3
|
+
*
|
|
4
|
+
* Lists `/repos/*.git` from inside the container with a single
|
|
5
|
+
* `docker exec -u git <container> ls -1 /repos`. This is a local call (no
|
|
6
|
+
* network) and fails soft: if Docker is missing or the container is stopped,
|
|
7
|
+
* it reports `available: false` and the panel simply omits the server line.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
const run = promisify(execFile);
|
|
12
|
+
export class ZgitCollector {
|
|
13
|
+
container;
|
|
14
|
+
inflight = false;
|
|
15
|
+
last;
|
|
16
|
+
constructor(container) {
|
|
17
|
+
this.container = container;
|
|
18
|
+
this.last = { available: false, container, repos: [] };
|
|
19
|
+
}
|
|
20
|
+
async read() {
|
|
21
|
+
if (!this.container || this.inflight)
|
|
22
|
+
return this.last;
|
|
23
|
+
this.inflight = true;
|
|
24
|
+
try {
|
|
25
|
+
const { stdout } = await run("docker", ["exec", "-u", "git", this.container, "ls", "-1", "/repos"], { maxBuffer: 1 << 16, timeout: 4000 });
|
|
26
|
+
const repos = stdout
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((s) => s.trim())
|
|
29
|
+
.filter((s) => s.endsWith(".git"))
|
|
30
|
+
.map((s) => s.replace(/\.git$/, ""))
|
|
31
|
+
.sort();
|
|
32
|
+
this.last = { available: true, container: this.container, repos };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
this.last = { available: false, container: this.container, repos: [] };
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
this.inflight = false;
|
|
39
|
+
}
|
|
40
|
+
return this.last;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,13 @@ export function loadConfig() {
|
|
|
20
20
|
cfg.theme = raw.theme;
|
|
21
21
|
if (raw.icons === "nerd" || raw.icons === "emoji" || raw.icons === "none")
|
|
22
22
|
cfg.icons = raw.icons;
|
|
23
|
+
if (Array.isArray(raw.repoRoots)) {
|
|
24
|
+
const roots = raw.repoRoots.filter((r) => typeof r === "string" && r.length > 0);
|
|
25
|
+
if (roots.length)
|
|
26
|
+
cfg.repoRoots = roots;
|
|
27
|
+
}
|
|
28
|
+
if (typeof raw.zgitContainer === "string" && raw.zgitContainer)
|
|
29
|
+
cfg.zgitContainer = raw.zgitContainer;
|
|
23
30
|
return cfg;
|
|
24
31
|
}
|
|
25
32
|
catch {
|
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",
|
|
@@ -39,13 +39,15 @@ export const en = {
|
|
|
39
39
|
"git.repo": "REPO",
|
|
40
40
|
"git.branch": "BRANCH",
|
|
41
41
|
"git.state": "STATE",
|
|
42
|
-
"git.
|
|
43
|
-
"git.
|
|
42
|
+
"git.host": "HOST",
|
|
43
|
+
"git.uncommitted": "uncommitted",
|
|
44
|
+
"git.allClear": "✓ all {n} repos up to date",
|
|
45
|
+
"git.local": "local",
|
|
46
|
+
"git.server": "— zgit server ({container}): {list} ({n})",
|
|
47
|
+
"git.serverEmpty": "— zgit server ({container}): no repos",
|
|
44
48
|
"gpu.usage": "Usage",
|
|
45
49
|
"gpu.mem": "mem {used}/{alloc}",
|
|
46
50
|
"temp.unavailable": "sensors unavailable",
|
|
47
|
-
"temp.waitingSudo": "waiting for sudo",
|
|
48
|
-
"temp.needsSudo": "power: requires sudo",
|
|
49
51
|
"spark.collecting": "collecting…",
|
|
50
52
|
"spark.lastMin": "last min: {range}",
|
|
51
53
|
"tokens.spent": "Spent today: ${cost} · {messages} messages",
|
|
@@ -70,7 +72,4 @@ export const en = {
|
|
|
70
72
|
"key.pause": "Pause",
|
|
71
73
|
"key.faster": "Faster",
|
|
72
74
|
"key.slower": "Slower",
|
|
73
|
-
"cli.sudoNeed": "lattice needs sudo to read power (powermetrics).",
|
|
74
|
-
"cli.sudoFail": "sudo unavailable — continuing without power data.",
|
|
75
|
-
"cli.sudoCancel": "cancelled.",
|
|
76
75
|
};
|
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",
|
|
@@ -38,13 +38,15 @@ export const es = {
|
|
|
38
38
|
"git.repo": "REPO",
|
|
39
39
|
"git.branch": "BRANCH",
|
|
40
40
|
"git.state": "ESTADO",
|
|
41
|
-
"git.
|
|
42
|
-
"git.
|
|
41
|
+
"git.host": "HOST",
|
|
42
|
+
"git.uncommitted": "sin commit",
|
|
43
|
+
"git.allClear": "✓ {n} repos al día",
|
|
44
|
+
"git.local": "local",
|
|
45
|
+
"git.server": "— servidor zgit ({container}): {list} ({n})",
|
|
46
|
+
"git.serverEmpty": "— servidor zgit ({container}): sin repos",
|
|
43
47
|
"gpu.usage": "Uso",
|
|
44
48
|
"gpu.mem": "mem {used}/{alloc}",
|
|
45
49
|
"temp.unavailable": "sensores no disponibles",
|
|
46
|
-
"temp.waitingSudo": "esperando sudo",
|
|
47
|
-
"temp.needsSudo": "energía: requiere sudo",
|
|
48
50
|
"spark.collecting": "recolectando…",
|
|
49
51
|
"spark.lastMin": "último min: {range}",
|
|
50
52
|
"tokens.spent": "Gastado hoy: ${cost} · {messages} mensajes",
|
|
@@ -69,7 +71,4 @@ export const es = {
|
|
|
69
71
|
"key.pause": "Pausar",
|
|
70
72
|
"key.faster": "Más rápido",
|
|
71
73
|
"key.slower": "Más lento",
|
|
72
|
-
"cli.sudoNeed": "lattice necesita sudo para leer la energía (powermetrics).",
|
|
73
|
-
"cli.sudoFail": "sudo no disponible — siguiendo sin datos de energía.",
|
|
74
|
-
"cli.sudoCancel": "cancelado.",
|
|
75
74
|
};
|
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",
|
|
@@ -38,13 +38,15 @@ export const ptBR = {
|
|
|
38
38
|
"git.repo": "REPO",
|
|
39
39
|
"git.branch": "BRANCH",
|
|
40
40
|
"git.state": "ESTADO",
|
|
41
|
-
"git.
|
|
42
|
-
"git.
|
|
41
|
+
"git.host": "HOST",
|
|
42
|
+
"git.uncommitted": "sem commit",
|
|
43
|
+
"git.allClear": "✓ todos os {n} repos em dia",
|
|
44
|
+
"git.local": "local",
|
|
45
|
+
"git.server": "— servidor zgit ({container}): {list} ({n})",
|
|
46
|
+
"git.serverEmpty": "— servidor zgit ({container}): sem repos",
|
|
43
47
|
"gpu.usage": "Uso",
|
|
44
48
|
"gpu.mem": "mem {used}/{alloc}",
|
|
45
49
|
"temp.unavailable": "sensores indisponíveis",
|
|
46
|
-
"temp.waitingSudo": "aguardando sudo",
|
|
47
|
-
"temp.needsSudo": "energia: requer sudo",
|
|
48
50
|
"spark.collecting": "coletando…",
|
|
49
51
|
"spark.lastMin": "último min: {range}",
|
|
50
52
|
"tokens.spent": "Gasto hoje: ${cost} · {messages} mensagens",
|
|
@@ -69,7 +71,4 @@ export const ptBR = {
|
|
|
69
71
|
"key.pause": "Pausar",
|
|
70
72
|
"key.faster": "Mais rápido",
|
|
71
73
|
"key.slower": "Mais lento",
|
|
72
|
-
"cli.sudoNeed": "lattice precisa de sudo para ler energia (powermetrics).",
|
|
73
|
-
"cli.sudoFail": "sudo indisponível — seguindo sem dados de energia.",
|
|
74
|
-
"cli.sudoCancel": "cancelado.",
|
|
75
74
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeluizr/lattice",
|
|
3
|
-
"version": "
|
|
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
|
-
"
|
|
37
|
+
"git",
|
|
38
38
|
"htop",
|
|
39
39
|
"ink",
|
|
40
40
|
"cli"
|
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
"bugs": {
|
|
50
50
|
"url": "https://github.com/zeluizr/lattice/issues"
|
|
51
51
|
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "restricted"
|
|
54
|
+
},
|
|
52
55
|
"scripts": {
|
|
53
56
|
"build": "tsc && node scripts/postbuild.mjs",
|
|
54
57
|
"build:native": "bash native/build.sh",
|
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
|
-
}
|