@zeluizr/lattice 1.0.0 → 1.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 +21 -0
- package/README.md +12 -1
- package/dist/app.js +34 -6
- package/dist/cli.js +36 -3
- package/dist/collectors/git.js +67 -21
- package/dist/collectors/zgit.js +42 -0
- package/dist/config.js +7 -0
- package/dist/i18n/en.js +4 -0
- package/dist/i18n/es.js +4 -0
- package/dist/i18n/pt-BR.js +4 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
|
|
5
5
|
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.1.0] — 2026-06-24
|
|
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 `--zgit` / config
|
|
17
|
+
`zgitContainer` (default `zgit`).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **The GIT panel is now cwd-independent.** It scans a fixed, configured list of
|
|
21
|
+
paths instead of the parent of the current directory, so the report is the
|
|
22
|
+
same in every terminal/tab. Each `--repos` entry may be a repo itself or a
|
|
23
|
+
folder whose subdirectories are repos; entries are merged and de-duplicated.
|
|
24
|
+
- `--repos` now accepts a comma-separated list and is persisted to
|
|
25
|
+
`~/.config/lattice/config.json` (`repoRoots`). With no flag and no saved list,
|
|
26
|
+
it falls back to the parent of the cwd (previous behaviour).
|
|
27
|
+
|
|
7
28
|
## [1.0.0] — 2026-06-24
|
|
8
29
|
|
|
9
30
|
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,6 +5,7 @@ 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
11
|
import { PowerCollector } from "./collectors/power.js";
|
|
@@ -18,11 +19,12 @@ function timeStr() {
|
|
|
18
19
|
return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
19
20
|
}
|
|
20
21
|
export function App(props) {
|
|
21
|
-
const { t, pal, icon, usePower, useVtex,
|
|
22
|
+
const { t, pal, icon, usePower, useVtex, repoRoots, zgitContainer, topN } = props;
|
|
22
23
|
const { exit } = useApp();
|
|
23
24
|
const [sys, setSys] = useState(null);
|
|
24
25
|
const [disks, setDisks] = useState(null);
|
|
25
26
|
const [git, setGit] = useState(null);
|
|
27
|
+
const [zgit, setZgit] = useState(null);
|
|
26
28
|
const [gpu, setGpu] = useState(null);
|
|
27
29
|
const [sensors, setSensors] = useState(null);
|
|
28
30
|
const [power, setPower] = useState(null);
|
|
@@ -40,6 +42,7 @@ export function App(props) {
|
|
|
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
47
|
const powerRef = useRef(null);
|
|
45
48
|
const pausedRef = useRef(false);
|
|
@@ -52,7 +55,8 @@ export function App(props) {
|
|
|
52
55
|
mounted.current = true;
|
|
53
56
|
sysRef.current = new SystemCollector(topN);
|
|
54
57
|
disksRef.current = new DisksCollector();
|
|
55
|
-
gitRef.current = new GitCollector(
|
|
58
|
+
gitRef.current = new GitCollector(repoRoots);
|
|
59
|
+
zgitRef.current = new ZgitCollector(zgitContainer);
|
|
56
60
|
tokRef.current = new TokenCollector();
|
|
57
61
|
if (usePower) {
|
|
58
62
|
powerRef.current = new PowerCollector(Math.round(props.interval * 1000));
|
|
@@ -98,16 +102,18 @@ export function App(props) {
|
|
|
98
102
|
const refreshAux = useCallback(async () => {
|
|
99
103
|
if (pausedRef.current || !tokRef.current)
|
|
100
104
|
return;
|
|
101
|
-
const [tk, vx, gt] = await Promise.all([
|
|
105
|
+
const [tk, vx, gt, zg] = await Promise.all([
|
|
102
106
|
tokRef.current.read(),
|
|
103
107
|
useVtex ? readVtex() : Promise.resolve(null),
|
|
104
108
|
gitRef.current ? gitRef.current.read() : Promise.resolve(null),
|
|
109
|
+
zgitRef.current ? zgitRef.current.read() : Promise.resolve(null),
|
|
105
110
|
]);
|
|
106
111
|
if (!mounted.current)
|
|
107
112
|
return;
|
|
108
113
|
setTokens(tk);
|
|
109
114
|
setVtex(vx);
|
|
110
115
|
setGit(gt);
|
|
116
|
+
setZgit(zg);
|
|
111
117
|
}, [useVtex]);
|
|
112
118
|
useEffect(() => {
|
|
113
119
|
refreshData();
|
|
@@ -164,6 +170,22 @@ export function App(props) {
|
|
|
164
170
|
const util = gpu?.utilPct ?? 0;
|
|
165
171
|
const diskRows = disks?.disks ?? [];
|
|
166
172
|
const gitRepos = git?.repos ?? [];
|
|
173
|
+
const zgitServer = zgit?.available ? zgit : null;
|
|
174
|
+
const showGit = gitRepos.length > 0 || !!zgitServer;
|
|
175
|
+
const hostLabel = (r) => r.hostKind === "github"
|
|
176
|
+
? "GitHub"
|
|
177
|
+
: r.hostKind === "zgit"
|
|
178
|
+
? "zgit"
|
|
179
|
+
: r.hostKind === "none"
|
|
180
|
+
? t("git.local")
|
|
181
|
+
: r.host || "—";
|
|
182
|
+
const hostColor = (r) => r.hostKind === "github"
|
|
183
|
+
? pal.purple
|
|
184
|
+
: r.hostKind === "zgit"
|
|
185
|
+
? pal.orange
|
|
186
|
+
: r.hostKind === "none"
|
|
187
|
+
? pal.comment
|
|
188
|
+
: pal.cyan;
|
|
167
189
|
const tokTotal = tokens ? tokens.input + tokens.output + tokens.cacheW + tokens.cacheR : 0;
|
|
168
190
|
const modelItems = Object.entries(tokens?.byModel ?? {}).sort((a, b) => b[1].cost - a[1].cost);
|
|
169
191
|
const modelParts = modelItems.slice(0, 2).map(([k, v]) => `${k} $${v.cost.toFixed(2)}`);
|
|
@@ -189,8 +211,14 @@ export function App(props) {
|
|
|
189
211
|
total: fmtTok(tokTotal),
|
|
190
212
|
input: fmtTok(tokens?.input),
|
|
191
213
|
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 }) })] }), useVtex && (_jsx(Panel, { title: `${icon("vtex")} ${t("panel.vtex")}`, color: pal.purple, width: col2, children: !vtex ? (_jsx(Text, { color: pal.comment, children: t("spark.collecting") })) : !vtex.installed ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.red, children: t("vtex.notInstalled") })] }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("vtex.install") })] })) : vtex.loggedIn ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.green, children: t("vtex.connected") })] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.account"), ": ", _jsx(Text, { bold: true, children: vtex.account })] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.user"), ": ", vtex.login || "—"] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.workspace"), ": ", vtex.workspace || "master"] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.yellow, children: t("vtex.notConnected") })] }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("vtex.signin") })] })) }))] }),
|
|
214
|
+
}) }), _jsx(Text, { wrap: "truncate", children: t("tokens.cache", { cw: fmtTok(tokens?.cacheW), cr: fmtTok(tokens?.cacheR) }) }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("tokens.byModel", { models }) })] }), useVtex && (_jsx(Panel, { title: `${icon("vtex")} ${t("panel.vtex")}`, color: pal.purple, width: col2, children: !vtex ? (_jsx(Text, { color: pal.comment, children: t("spark.collecting") })) : !vtex.installed ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.red, children: t("vtex.notInstalled") })] }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("vtex.install") })] })) : vtex.loggedIn ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.green, children: t("vtex.connected") })] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.account"), ": ", _jsx(Text, { bold: true, children: vtex.account })] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.user"), ": ", vtex.login || "—"] }), _jsxs(Text, { wrap: "truncate", children: [t("vtex.workspace"), ": ", vtex.workspace || "master"] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [t("vtex.status"), ": ", _jsx(Text, { color: pal.yellow, children: t("vtex.notConnected") })] }), _jsx(Text, { wrap: "truncate", color: pal.comment, children: t("vtex.signin") })] })) }))] }), showGit && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.green, paddingX: 1, marginRight: 1, width: fullW, children: [_jsxs(Text, { color: pal.green, bold: true, wrap: "truncate", children: [icon("git"), " ", t("panel.git"), git?.truncated ? _jsx(Text, { color: pal.comment, children: " (+)" }) : null] }), gitRepos.length > 0 && (_jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("git.repo").padEnd(20), t("git.branch").padEnd(18), t("git.state").padEnd(10), t("git.host")] })), gitRepos.map((r) => {
|
|
193
215
|
const label = r.detached ? "(detached)" : r.branch;
|
|
194
|
-
return (_jsxs(Text, { wrap: "truncate", children: [r.name.slice(0, 19).padEnd(20), label.slice(0,
|
|
195
|
-
})
|
|
216
|
+
return (_jsxs(Text, { wrap: "truncate", children: [r.name.slice(0, 19).padEnd(20), label.slice(0, 17).padEnd(18), _jsx(Text, { color: r.dirty ? pal.yellow : pal.green, children: "\u25CF" }), ` ${r.dirty ? t("git.dirty") : t("git.clean")}`.padEnd(9), _jsx(Text, { color: hostColor(r), children: hostLabel(r).slice(0, 10).padEnd(10) }), r.ahead > 0 ? _jsxs(Text, { color: pal.comment, children: [" \u2191", r.ahead] }) : null, r.behind > 0 ? _jsxs(Text, { color: pal.comment, children: [" \u2193", r.behind] }) : null] }, r.path));
|
|
217
|
+
}), zgitServer && (_jsx(Text, { color: pal.comment, wrap: "truncate", children: zgitServer.repos.length
|
|
218
|
+
? t("git.server", {
|
|
219
|
+
container: zgitServer.container,
|
|
220
|
+
list: zgitServer.repos.join(", "),
|
|
221
|
+
n: zgitServer.repos.length,
|
|
222
|
+
})
|
|
223
|
+
: t("git.serverEmpty", { container: zgitServer.container }) }))] })), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.comment, paddingX: 1, marginRight: 1, width: fullW, children: [_jsxs(Text, { color: pal.comment, bold: true, children: [icon("proc"), " ", t("panel.procs")] }), _jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("proc.cpu").padEnd(6), t("proc.mem").padEnd(9), t("proc.pid").padEnd(8), t("proc.name")] }), (sys?.procs ?? []).map((p) => (_jsxs(Text, { wrap: "truncate", children: [`${p.cpu.toFixed(0)}%`.padEnd(6), humanBytes(p.rss).padEnd(9), String(p.pid).padEnd(8), p.name.slice(0, 30)] }, p.pid)))] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: pal.comment, children: [_jsx(Text, { color: pal.pink, children: "q" }), " ", t("key.quit"), " \u00B7 ", _jsx(Text, { color: pal.pink, children: "p" }), " ", t("key.pause"), " \u00B7", " ", _jsx(Text, { color: pal.pink, children: "+/-" }), " ", t("key.faster"), "/", t("key.slower"), " \u00B7 ", interval.toFixed(2), "s"] }) })] }));
|
|
196
224
|
}
|
package/dist/cli.js
CHANGED
|
@@ -19,8 +19,13 @@ const cli = meow(`
|
|
|
19
19
|
Options
|
|
20
20
|
--no-power skip powermetrics/sudo (CPU/GPU/RAM/disk/net only)
|
|
21
21
|
--no-vtex hide the VTEX panel (for non-VTEX users)
|
|
22
|
-
--repos
|
|
23
|
-
|
|
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)
|
|
24
29
|
--interval, -i refresh interval in seconds (default 1)
|
|
25
30
|
--procs, -n number of top processes to show (default 8)
|
|
26
31
|
--icons icon style: nerd | emoji | none (default nerd)
|
|
@@ -40,6 +45,7 @@ const cli = meow(`
|
|
|
40
45
|
power: { type: "boolean", default: true },
|
|
41
46
|
vtex: { type: "boolean", default: true },
|
|
42
47
|
repos: { type: "string", default: "" },
|
|
48
|
+
zgit: { type: "string", default: "" },
|
|
43
49
|
interval: { type: "number", shortFlag: "i", default: 1 },
|
|
44
50
|
procs: { type: "number", shortFlag: "n", default: 8 },
|
|
45
51
|
icons: { type: "string", default: "" },
|
|
@@ -88,10 +94,37 @@ function resolveIcons() {
|
|
|
88
94
|
}
|
|
89
95
|
return loadConfig().icons ?? "nerd";
|
|
90
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
|
+
}
|
|
91
122
|
async function main() {
|
|
92
123
|
const lang = await resolveLang();
|
|
93
124
|
const variant = resolveTheme();
|
|
94
125
|
const iconMode = resolveIcons();
|
|
126
|
+
const repoRoots = resolveRepoRoots();
|
|
127
|
+
const zgitContainer = resolveZgitContainer();
|
|
95
128
|
const t = makeT(lang);
|
|
96
129
|
const pal = palette(variant);
|
|
97
130
|
const icon = makeIcons(iconMode);
|
|
@@ -107,7 +140,7 @@ async function main() {
|
|
|
107
140
|
usePower = false;
|
|
108
141
|
}
|
|
109
142
|
}
|
|
110
|
-
const { waitUntilExit } = render(_jsx(App, { t: t, pal: pal, icon: icon, lang: lang, usePower: usePower, useVtex: cli.flags.vtex,
|
|
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 }));
|
|
111
144
|
await waitUntilExit();
|
|
112
145
|
}
|
|
113
146
|
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
|
|
@@ -19,24 +23,19 @@ const run = promisify(execFile);
|
|
|
19
23
|
const MAX_REPOS = 24;
|
|
20
24
|
const CONCURRENCY = 8;
|
|
21
25
|
export class GitCollector {
|
|
22
|
-
|
|
26
|
+
roots;
|
|
23
27
|
inflight = false;
|
|
24
28
|
last;
|
|
25
|
-
constructor(
|
|
26
|
-
this.
|
|
27
|
-
this.last = {
|
|
29
|
+
constructor(roots) {
|
|
30
|
+
this.roots = roots;
|
|
31
|
+
this.last = { roots, repos: [], truncated: false };
|
|
28
32
|
}
|
|
29
33
|
async read() {
|
|
30
34
|
if (this.inflight)
|
|
31
35
|
return this.last;
|
|
32
36
|
this.inflight = true;
|
|
33
37
|
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();
|
|
38
|
+
const dirs = await this.discover();
|
|
40
39
|
const truncated = dirs.length > MAX_REPOS;
|
|
41
40
|
const pick = dirs.slice(0, MAX_REPOS);
|
|
42
41
|
const repos = [];
|
|
@@ -48,7 +47,7 @@ export class GitCollector {
|
|
|
48
47
|
repos.push(r);
|
|
49
48
|
}
|
|
50
49
|
repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
51
|
-
this.last = {
|
|
50
|
+
this.last = { roots: this.roots, repos, truncated };
|
|
52
51
|
return this.last;
|
|
53
52
|
}
|
|
54
53
|
catch {
|
|
@@ -58,17 +57,48 @@ export class GitCollector {
|
|
|
58
57
|
this.inflight = false;
|
|
59
58
|
}
|
|
60
59
|
}
|
|
60
|
+
/** Resolve every configured root to a de-duplicated, sorted list of repo dirs. */
|
|
61
|
+
async discover() {
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const out = [];
|
|
64
|
+
const add = (p) => {
|
|
65
|
+
if (!seen.has(p)) {
|
|
66
|
+
seen.add(p);
|
|
67
|
+
out.push(p);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
for (const root of this.roots) {
|
|
71
|
+
// A configured path can be a repo itself…
|
|
72
|
+
if (existsSync(join(root, ".git"))) {
|
|
73
|
+
add(root);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// …or a folder whose immediate children are repos.
|
|
77
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
if (!e.isDirectory() || e.name.startsWith("."))
|
|
80
|
+
continue;
|
|
81
|
+
const p = join(root, e.name);
|
|
82
|
+
if (existsSync(join(p, ".git")))
|
|
83
|
+
add(p);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out.sort();
|
|
87
|
+
}
|
|
61
88
|
}
|
|
62
89
|
async function readRepo(dir) {
|
|
63
|
-
const
|
|
64
|
-
maxBuffer: 1 << 20,
|
|
65
|
-
|
|
90
|
+
const [statusRes, urlRes] = await Promise.all([
|
|
91
|
+
run("git", ["-C", dir, "status", "--porcelain=v2", "--branch"], { maxBuffer: 1 << 20 }),
|
|
92
|
+
run("git", ["-C", dir, "remote", "get-url", "origin"], { maxBuffer: 1 << 16 }).catch(() => ({
|
|
93
|
+
stdout: "",
|
|
94
|
+
})),
|
|
95
|
+
]);
|
|
66
96
|
let branch = "?";
|
|
67
97
|
let ahead = 0;
|
|
68
98
|
let behind = 0;
|
|
69
99
|
let detached = false;
|
|
70
100
|
let dirty = false;
|
|
71
|
-
for (const ln of stdout.split("\n")) {
|
|
101
|
+
for (const ln of statusRes.stdout.split("\n")) {
|
|
72
102
|
if (ln.startsWith("# branch.head ")) {
|
|
73
103
|
branch = ln.slice("# branch.head ".length).trim();
|
|
74
104
|
if (branch === "(detached)")
|
|
@@ -85,5 +115,21 @@ async function readRepo(dir) {
|
|
|
85
115
|
dirty = true;
|
|
86
116
|
}
|
|
87
117
|
}
|
|
88
|
-
|
|
118
|
+
const { host, hostKind } = classifyHost(urlRes.stdout.trim());
|
|
119
|
+
return { name: basename(dir), path: dir, branch, ahead, behind, detached, dirty, host, hostKind };
|
|
120
|
+
}
|
|
121
|
+
/** Pull the hostname out of an origin URL and bucket it into a known host. */
|
|
122
|
+
export function classifyHost(url) {
|
|
123
|
+
if (!url)
|
|
124
|
+
return { host: "", hostKind: "none" };
|
|
125
|
+
// https://host/…, ssh://git@host/… or scp-like git@host:path
|
|
126
|
+
const scheme = url.match(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i);
|
|
127
|
+
const scp = url.match(/^[^/@]+@([^:/]+):/);
|
|
128
|
+
const host = (scheme?.[1] || scp?.[1] || "").toLowerCase();
|
|
129
|
+
let hostKind = "other";
|
|
130
|
+
if (host.endsWith("github.com"))
|
|
131
|
+
hostKind = "github";
|
|
132
|
+
else if (host.includes("zgit"))
|
|
133
|
+
hostKind = "zgit";
|
|
134
|
+
return { host, hostKind };
|
|
89
135
|
}
|
|
@@ -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
|
@@ -39,8 +39,12 @@ export const en = {
|
|
|
39
39
|
"git.repo": "REPO",
|
|
40
40
|
"git.branch": "BRANCH",
|
|
41
41
|
"git.state": "STATE",
|
|
42
|
+
"git.host": "HOST",
|
|
42
43
|
"git.clean": "clean",
|
|
43
44
|
"git.dirty": "dirty",
|
|
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",
|
package/dist/i18n/es.js
CHANGED
|
@@ -38,8 +38,12 @@ export const es = {
|
|
|
38
38
|
"git.repo": "REPO",
|
|
39
39
|
"git.branch": "BRANCH",
|
|
40
40
|
"git.state": "ESTADO",
|
|
41
|
+
"git.host": "HOST",
|
|
41
42
|
"git.clean": "limpio",
|
|
42
43
|
"git.dirty": "sucio",
|
|
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",
|
package/dist/i18n/pt-BR.js
CHANGED
|
@@ -38,8 +38,12 @@ export const ptBR = {
|
|
|
38
38
|
"git.repo": "REPO",
|
|
39
39
|
"git.branch": "BRANCH",
|
|
40
40
|
"git.state": "ESTADO",
|
|
41
|
+
"git.host": "HOST",
|
|
41
42
|
"git.clean": "limpo",
|
|
42
43
|
"git.dirty": "sujo",
|
|
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",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeluizr/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.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": {
|
|
@@ -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",
|