@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 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
- No install needed run it straight from npm:
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, gitDir, topN } = props;
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(gitDir);
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") })] })) }))] }), gitRepos.length > 0 && (_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] }), _jsxs(Text, { color: pal.purple, bold: true, wrap: "truncate", children: [t("git.repo").padEnd(20), t("git.branch").padEnd(22), t("git.state")] }), gitRepos.map((r) => {
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, 21).padEnd(22), _jsxs(Text, { color: r.dirty ? pal.yellow : pal.green, children: ["\u25CF ", r.dirty ? t("git.dirty") : t("git.clean")] }), 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.name));
195
- })] })), _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"] }) })] }));
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 folder of git repos to show branches for
23
- (default: the parent of the current directory)
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, gitDir: cli.flags.repos || dirname(process.cwd()), interval: cli.flags.interval, topN: cli.flags.procs }));
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) => {
@@ -1,10 +1,14 @@
1
1
  /**
2
- * Active git branches for the repos in a folder (no sudo).
2
+ * Active git branches for a fixed, configured set of repos (no sudo).
3
3
  *
4
- * Scans the immediate subdirectories of `root`, keeps the ones that are git
5
- * repositories, and reports each repo's current branch plus its state —
6
- * dirty/clean and ahead/behind its upstream from a single
7
- * `git status --porcelain=v2 --branch` per repo.
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
- root;
26
+ roots;
23
27
  inflight = false;
24
28
  last;
25
- constructor(root) {
26
- this.root = root;
27
- this.last = { root, repos: [], truncated: false };
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 entries = await readdir(this.root, { withFileTypes: true }).catch(() => []);
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 = { root: this.root, repos, truncated };
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 { stdout } = await run("git", ["-C", dir, "status", "--porcelain=v2", "--branch"], {
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
- return { name: basename(dir), branch, ahead, behind, detached, dirty };
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",
@@ -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.0.0",
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",