@zeluizr/lattice 1.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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Active git branches for the repos in a folder (no sudo).
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.
8
+ *
9
+ * Cheap to discover (one readdir + a stat per child) but each repo costs a git
10
+ * invocation, so we cap the count, bound concurrency, and skip overlapping
11
+ * reads (returning the previous result while one is still in flight).
12
+ */
13
+ import { execFile } from "node:child_process";
14
+ import { promisify } from "node:util";
15
+ import { readdir } from "node:fs/promises";
16
+ import { existsSync } from "node:fs";
17
+ import { join, basename } from "node:path";
18
+ const run = promisify(execFile);
19
+ const MAX_REPOS = 24;
20
+ const CONCURRENCY = 8;
21
+ export class GitCollector {
22
+ root;
23
+ inflight = false;
24
+ last;
25
+ constructor(root) {
26
+ this.root = root;
27
+ this.last = { root, repos: [], truncated: false };
28
+ }
29
+ async read() {
30
+ if (this.inflight)
31
+ return this.last;
32
+ this.inflight = true;
33
+ 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();
40
+ const truncated = dirs.length > MAX_REPOS;
41
+ const pick = dirs.slice(0, MAX_REPOS);
42
+ const repos = [];
43
+ for (let i = 0; i < pick.length; i += CONCURRENCY) {
44
+ const batch = pick.slice(i, i + CONCURRENCY);
45
+ const results = await Promise.all(batch.map((d) => readRepo(d).catch(() => null)));
46
+ for (const r of results)
47
+ if (r)
48
+ repos.push(r);
49
+ }
50
+ repos.sort((a, b) => a.name.localeCompare(b.name));
51
+ this.last = { root: this.root, repos, truncated };
52
+ return this.last;
53
+ }
54
+ catch {
55
+ return this.last;
56
+ }
57
+ finally {
58
+ this.inflight = false;
59
+ }
60
+ }
61
+ }
62
+ async function readRepo(dir) {
63
+ const { stdout } = await run("git", ["-C", dir, "status", "--porcelain=v2", "--branch"], {
64
+ maxBuffer: 1 << 20,
65
+ });
66
+ let branch = "?";
67
+ let ahead = 0;
68
+ let behind = 0;
69
+ let detached = false;
70
+ let dirty = false;
71
+ for (const ln of stdout.split("\n")) {
72
+ if (ln.startsWith("# branch.head ")) {
73
+ branch = ln.slice("# branch.head ".length).trim();
74
+ if (branch === "(detached)")
75
+ detached = true;
76
+ }
77
+ else if (ln.startsWith("# branch.ab ")) {
78
+ const m = ln.match(/\+(\d+)\s+-(\d+)/);
79
+ if (m) {
80
+ ahead = Number(m[1]);
81
+ behind = Number(m[2]);
82
+ }
83
+ }
84
+ else if (ln && !ln.startsWith("#")) {
85
+ dirty = true;
86
+ }
87
+ }
88
+ return { name: basename(dir), branch, ahead, behind, detached, dirty };
89
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Apple Silicon GPU via `ioreg` (no sudo). Parses utilization and system
3
+ * memory in use / allocated from the IOAccelerator registry entry.
4
+ */
5
+ import { execa } from "execa";
6
+ const EMPTY = { utilPct: null, memUsedBytes: null, memAllocBytes: null };
7
+ export async function readGpu() {
8
+ try {
9
+ const { stdout } = await execa("ioreg", ["-r", "-d", "1", "-w", "0", "-c", "IOAccelerator"], {
10
+ timeout: 2000,
11
+ });
12
+ const get = (key) => {
13
+ const m = stdout.match(new RegExp(`"${escapeRe(key)}"\\s*=\\s*(\\d+)`));
14
+ return m ? Number(m[1]) : null;
15
+ };
16
+ return {
17
+ utilPct: get("Device Utilization %"),
18
+ memUsedBytes: get("In use system memory"),
19
+ memAllocBytes: get("Alloc system memory"),
20
+ };
21
+ }
22
+ catch {
23
+ return { ...EMPTY };
24
+ }
25
+ }
26
+ function escapeRe(s) {
27
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
@@ -0,0 +1,105 @@
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
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Temperatures (CPU/GPU) and fans via the native lattice-smc helper (no sudo),
3
+ * plus battery via systeminformation + ioreg. On any failure the sensors panel
4
+ * degrades gracefully without taking the app down.
5
+ */
6
+ import { fileURLToPath } from "node:url";
7
+ import { existsSync } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import { execa } from "execa";
10
+ import si from "systeminformation";
11
+ const HERE = dirname(fileURLToPath(import.meta.url));
12
+ const BIN_NAME = "lattice-smc";
13
+ const PLATFORM_DIR = `${process.platform}-${process.arch}`;
14
+ function locateHelper() {
15
+ const candidates = [
16
+ // dist/collectors -> package root
17
+ join(HERE, "..", "..", "prebuilds", PLATFORM_DIR, BIN_NAME),
18
+ // running from source layout
19
+ join(HERE, "..", "..", "..", "prebuilds", PLATFORM_DIR, BIN_NAME),
20
+ join(process.cwd(), "prebuilds", PLATFORM_DIR, BIN_NAME),
21
+ ];
22
+ return candidates.find((p) => existsSync(p)) ?? null;
23
+ }
24
+ const HELPER = locateHelper();
25
+ export function sensorsAvailable() {
26
+ return HELPER !== null;
27
+ }
28
+ export async function readSensors() {
29
+ if (!HELPER) {
30
+ return { ok: false, error: "smc helper not found", cpuTemp: null, gpuTemp: null, fans: [] };
31
+ }
32
+ try {
33
+ const { stdout } = await execa(HELPER, [], { timeout: 1500 });
34
+ const j = JSON.parse(stdout);
35
+ if (!j.ok) {
36
+ return { ok: false, error: j.error, cpuTemp: null, gpuTemp: null, fans: [] };
37
+ }
38
+ return {
39
+ ok: true,
40
+ cpuTemp: j.cpu_temp ?? null,
41
+ gpuTemp: j.gpu_temp ?? null,
42
+ fans: Array.isArray(j.fans) ? j.fans : [],
43
+ };
44
+ }
45
+ catch (e) {
46
+ return { ok: false, error: String(e), cpuTemp: null, gpuTemp: null, fans: [] };
47
+ }
48
+ }
49
+ export async function readBattery() {
50
+ try {
51
+ const b = await si.battery();
52
+ if (!b || !b.hasBattery)
53
+ return { present: false };
54
+ const health = b.maxCapacity && b.designedCapacity ? (b.maxCapacity / b.designedCapacity) * 100 : null;
55
+ let tempC = null;
56
+ try {
57
+ const { stdout } = await execa("ioreg", ["-rn", "AppleSmartBattery", "-w", "0"], {
58
+ timeout: 1500,
59
+ });
60
+ const m = stdout.match(/"Temperature"\s*=\s*(-?\d+)/);
61
+ if (m)
62
+ tempC = Number(m[1]) / 100;
63
+ }
64
+ catch {
65
+ // optional
66
+ }
67
+ return {
68
+ present: true,
69
+ percent: b.percent,
70
+ plugged: Boolean(b.acConnected),
71
+ charging: Boolean(b.isCharging),
72
+ cycles: b.cycleCount ?? null,
73
+ health,
74
+ tempC,
75
+ };
76
+ }
77
+ catch {
78
+ return { present: false };
79
+ }
80
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * System metrics via `systeminformation` (no sudo): CPU load, memory/swap,
3
+ * disk & network throughput, '/' usage and the top processes by CPU.
4
+ *
5
+ * systeminformation computes per-second rates (network/disk) relative to the
6
+ * previous call, so the first sample may be approximate; subsequent ones are
7
+ * accurate at the configured refresh interval.
8
+ */
9
+ import si from "systeminformation";
10
+ export class SystemCollector {
11
+ topN;
12
+ constructor(topN = 8) {
13
+ this.topN = topN;
14
+ }
15
+ async read() {
16
+ const [load, mem, fs, net, sizes, procs] = await Promise.all([
17
+ si.currentLoad().catch(() => null),
18
+ si.mem().catch(() => null),
19
+ si.fsStats().catch(() => null),
20
+ si.networkStats().catch(() => null),
21
+ si.fsSize().catch(() => null),
22
+ si.processes().catch(() => null),
23
+ ]);
24
+ const cpuTotal = load?.currentLoad ?? 0;
25
+ const cpuPer = (load?.cpus ?? []).map((c) => c.load ?? 0);
26
+ const memTotal = mem?.total ?? 0;
27
+ const memAvail = mem?.available ?? mem?.free ?? 0;
28
+ const memUsed = Math.max(0, memTotal - memAvail);
29
+ const memPercent = memTotal > 0 ? (memUsed / memTotal) * 100 : 0;
30
+ const swapTotal = mem?.swaptotal ?? 0;
31
+ const swapPercent = swapTotal > 0 ? ((mem?.swapused ?? 0) / swapTotal) * 100 : 0;
32
+ const diskReadBps = nonNeg(fs?.rx_sec);
33
+ const diskWriteBps = nonNeg(fs?.wx_sec);
34
+ let netRecvBps = 0;
35
+ let netSentBps = 0;
36
+ for (const n of net ?? []) {
37
+ netRecvBps += nonNeg(n.rx_sec);
38
+ netSentBps += nonNeg(n.tx_sec);
39
+ }
40
+ const root = (sizes ?? []).find((s) => s.mount === "/") ?? (sizes ?? [])[0];
41
+ const diskUsagePercent = root?.use ?? 0;
42
+ const list = procs?.list ?? [];
43
+ const top = list
44
+ .map((p) => ({
45
+ cpu: p.cpu ?? 0,
46
+ pid: p.pid ?? 0,
47
+ name: p.name ?? "?",
48
+ // memRss is in KB on most platforms; fall back to mem% of total RAM.
49
+ rss: p.memRss ? p.memRss * 1024 : Math.round(((p.mem ?? 0) / 100) * memTotal),
50
+ }))
51
+ .sort((a, b) => b.cpu - a.cpu)
52
+ .slice(0, this.topN);
53
+ return {
54
+ cpuTotal,
55
+ cpuPer,
56
+ memPercent,
57
+ memUsed,
58
+ memTotal,
59
+ swapPercent,
60
+ diskReadBps,
61
+ diskWriteBps,
62
+ netRecvBps,
63
+ netSentBps,
64
+ diskUsagePercent,
65
+ procs: top,
66
+ };
67
+ }
68
+ }
69
+ function nonNeg(v) {
70
+ return typeof v === "number" && v > 0 ? v : 0;
71
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Claude Code token usage / cost for TODAY, read from local logs.
3
+ *
4
+ * Claude Code writes transcripts to ~/.claude/projects/**\/*.jsonl. Each
5
+ * assistant line carries message.usage (input/output + cache tokens), the model
6
+ * and an ISO timestamp. We sum everything from the local day and compute cost.
7
+ *
8
+ * Pricing (USD per 1M tokens) — source: Anthropic claude-api skill (Jun 2026):
9
+ * Opus 4.8 in $5 out $25 cache-write $6.25 cache-read $0.50
10
+ * Sonnet 4.6 in $3 out $15 cache-write $3.75 cache-read $0.30
11
+ * Haiku 4.5 in $1 out $5 cache-write $1.25 cache-read $0.10
12
+ *
13
+ * Performance: on first read we skip files not modified today; afterwards we
14
+ * tail only the new bytes of each file.
15
+ */
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { readdir, stat, open } from "node:fs/promises";
19
+ const PRICING = {
20
+ opus: { in: 5.0, out: 25.0, cw: 6.25, cr: 0.5 },
21
+ sonnet: { in: 3.0, out: 15.0, cw: 3.75, cr: 0.3 },
22
+ haiku: { in: 1.0, out: 5.0, cw: 1.25, cr: 0.1 },
23
+ };
24
+ function family(model) {
25
+ const m = (model || "").toLowerCase();
26
+ if (m.includes("opus"))
27
+ return "opus";
28
+ if (m.includes("sonnet"))
29
+ return "sonnet";
30
+ if (m.includes("haiku"))
31
+ return "haiku";
32
+ return "opus"; // conservative default
33
+ }
34
+ function localDateISO(d) {
35
+ const y = d.getFullYear();
36
+ const mo = String(d.getMonth() + 1).padStart(2, "0");
37
+ const da = String(d.getDate()).padStart(2, "0");
38
+ return `${y}-${mo}-${da}`;
39
+ }
40
+ function zero() {
41
+ return {
42
+ input: 0,
43
+ output: 0,
44
+ cacheW: 0,
45
+ cacheR: 0,
46
+ cost: 0,
47
+ webSearch: 0,
48
+ webFetch: 0,
49
+ messages: 0,
50
+ byModel: {},
51
+ };
52
+ }
53
+ export class TokenCollector {
54
+ root;
55
+ offsets = new Map();
56
+ day = null;
57
+ totals = zero();
58
+ constructor(root) {
59
+ this.root = root ?? join(homedir(), ".claude", "projects");
60
+ }
61
+ async read() {
62
+ const now = new Date();
63
+ const today = localDateISO(now);
64
+ if (today !== this.day) {
65
+ this.day = today;
66
+ this.offsets.clear();
67
+ this.totals = zero();
68
+ }
69
+ const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
70
+ let files = [];
71
+ try {
72
+ const entries = await readdir(this.root, { recursive: true, withFileTypes: true });
73
+ files = entries
74
+ .filter((e) => e.isFile() && e.name.endsWith(".jsonl"))
75
+ .map((e) => join(e.parentPath, e.name));
76
+ }
77
+ catch {
78
+ return this.totals;
79
+ }
80
+ for (const path of files) {
81
+ let size;
82
+ let mtimeMs;
83
+ try {
84
+ const st = await stat(path);
85
+ size = st.size;
86
+ mtimeMs = st.mtimeMs;
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ let off = this.offsets.get(path);
92
+ if (off === undefined) {
93
+ if (mtimeMs < midnight) {
94
+ this.offsets.set(path, size); // nothing from today here
95
+ continue;
96
+ }
97
+ off = 0;
98
+ }
99
+ if (size < off)
100
+ off = 0; // rotated / truncated
101
+ if (size === off)
102
+ continue;
103
+ let buf;
104
+ try {
105
+ const fh = await open(path, "r");
106
+ try {
107
+ const length = size - off;
108
+ const b = Buffer.alloc(length);
109
+ await fh.read(b, 0, length, off);
110
+ buf = b;
111
+ }
112
+ finally {
113
+ await fh.close();
114
+ }
115
+ }
116
+ catch {
117
+ continue;
118
+ }
119
+ let data = buf;
120
+ if (data.length && data[data.length - 1] !== 0x0a) {
121
+ const cut = data.lastIndexOf(0x0a);
122
+ if (cut === -1)
123
+ continue; // no complete line yet; keep offset
124
+ this.offsets.set(path, off + cut + 1);
125
+ data = data.subarray(0, cut + 1);
126
+ }
127
+ else {
128
+ this.offsets.set(path, size);
129
+ }
130
+ for (const line of data.toString("utf8").split("\n")) {
131
+ if (line.trim())
132
+ this.consume(line, today);
133
+ }
134
+ }
135
+ return this.totals;
136
+ }
137
+ consume(line, today) {
138
+ let o;
139
+ try {
140
+ o = JSON.parse(line);
141
+ }
142
+ catch {
143
+ return;
144
+ }
145
+ const ts = o?.timestamp;
146
+ if (!ts)
147
+ return;
148
+ let d;
149
+ try {
150
+ d = localDateISO(new Date(ts));
151
+ }
152
+ catch {
153
+ return;
154
+ }
155
+ if (d !== today)
156
+ return;
157
+ const u = o?.message?.usage;
158
+ if (!u)
159
+ return;
160
+ const fam = family(o?.message?.model);
161
+ const p = PRICING[fam];
162
+ const i = u.input_tokens || 0;
163
+ const out = u.output_tokens || 0;
164
+ const cw = u.cache_creation_input_tokens || 0;
165
+ const cr = u.cache_read_input_tokens || 0;
166
+ const cost = (i * p.in + out * p.out + cw * p.cw + cr * p.cr) / 1_000_000;
167
+ const t = this.totals;
168
+ t.input += i;
169
+ t.output += out;
170
+ t.cacheW += cw;
171
+ t.cacheR += cr;
172
+ t.cost += cost;
173
+ t.messages += 1;
174
+ const stu = u.server_tool_use || {};
175
+ t.webSearch += stu.web_search_requests || 0;
176
+ t.webFetch += stu.web_fetch_requests || 0;
177
+ const bm = t.byModel[fam] ?? { in: 0, out: 0, cw: 0, cr: 0, cost: 0 };
178
+ bm.in += i;
179
+ bm.out += out;
180
+ bm.cw += cw;
181
+ bm.cr += cr;
182
+ bm.cost += cost;
183
+ t.byModel[fam] = bm;
184
+ }
185
+ }
@@ -0,0 +1,2 @@
1
+ /** Shared data shapes produced by the collectors. */
2
+ export {};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * VTEX CLI session status, read from its configstore file. No sudo, no network.
3
+ */
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { readFile } from "node:fs/promises";
7
+ import { execa } from "execa";
8
+ const CONFIG = join(homedir(), ".config", "configstore", "vtex.json");
9
+ async function whichVtex() {
10
+ try {
11
+ const { stdout } = await execa("which", ["vtex"], { timeout: 1500 });
12
+ return stdout.trim() || null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export async function readVtex() {
19
+ const path = await whichVtex();
20
+ const installed = path !== null;
21
+ let account = null;
22
+ let login = null;
23
+ let workspace = null;
24
+ let token = null;
25
+ try {
26
+ const raw = JSON.parse(await readFile(CONFIG, "utf8"));
27
+ account = raw.account ?? null;
28
+ login = raw.login ?? null;
29
+ workspace = raw.workspace ?? null;
30
+ token = raw.token ?? null;
31
+ }
32
+ catch {
33
+ // file missing/unreadable → treated as logged out
34
+ }
35
+ const loggedIn = Boolean(account && (token || login));
36
+ return { installed, path, account, login, workspace, loggedIn };
37
+ }
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { LANGS } from "../i18n/index.js";
5
+ /** First-run language picker. Trilingual prompt so it reads for everyone. */
6
+ export function LanguageSelect({ pal, onSelect, }) {
7
+ const [index, setIndex] = useState(0);
8
+ useInput((input, key) => {
9
+ if (key.upArrow || input === "k")
10
+ setIndex((i) => (i - 1 + LANGS.length) % LANGS.length);
11
+ else if (key.downArrow || input === "j")
12
+ setIndex((i) => (i + 1) % LANGS.length);
13
+ else if (key.return)
14
+ onSelect(LANGS[index].code);
15
+ else {
16
+ const n = Number(input);
17
+ if (n >= 1 && n <= LANGS.length)
18
+ onSelect(LANGS[n - 1].code);
19
+ }
20
+ });
21
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: pal.purple, paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: pal.purple, bold: true, children: "lattice" }), _jsx(Text, { color: pal.comment, children: "Choose your language \u00B7 Elige tu idioma \u00B7 Escolha seu idioma" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: LANGS.map((l, i) => {
22
+ const active = i === index;
23
+ return (_jsxs(Text, { color: active ? pal.cyan : pal.foreground, children: [active ? "❯ " : " ", i + 1, ". ", l.label] }, l.code));
24
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: pal.comment, children: "\u2191/\u2193 move \u00B7 Enter select \u00B7 1\u20133 quick pick" }) })] }));
25
+ }
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ /** A bordered card with a colored title, matching the original Dracula Pro look. */
4
+ export function Panel({ title, color, children, width, minHeight, }) {
5
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingX: 1, marginRight: 1, flexGrow: width ? 0 : 1, flexShrink: 0, width: width, minHeight: minHeight, children: [_jsx(Text, { color: color, bold: true, wrap: "truncate", children: title }), children] }));
6
+ }
package/dist/config.js ADDED
@@ -0,0 +1,42 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
4
+ import { isLang } from "./i18n/index.js";
5
+ import { isVariant } from "./theme.js";
6
+ const DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "lattice");
7
+ const FILE = join(DIR, "config.json");
8
+ export function configPath() {
9
+ return FILE;
10
+ }
11
+ export function loadConfig() {
12
+ try {
13
+ if (!existsSync(FILE))
14
+ return {};
15
+ const raw = JSON.parse(readFileSync(FILE, "utf8"));
16
+ const cfg = {};
17
+ if (raw.lang && isLang(raw.lang))
18
+ cfg.lang = raw.lang;
19
+ if (raw.theme && isVariant(raw.theme))
20
+ cfg.theme = raw.theme;
21
+ if (raw.icons === "nerd" || raw.icons === "emoji" || raw.icons === "none")
22
+ cfg.icons = raw.icons;
23
+ return cfg;
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ export function saveConfig(cfg) {
30
+ try {
31
+ mkdirSync(DIR, { recursive: true });
32
+ const current = loadConfig();
33
+ writeFileSync(FILE, JSON.stringify({ ...current, ...cfg }, null, 2) + "\n", "utf8");
34
+ }
35
+ catch {
36
+ // best effort — config persistence is non-critical
37
+ }
38
+ }
39
+ /** True when no language has been chosen yet (drives the first-run picker). */
40
+ export function needsLanguageSetup() {
41
+ return !loadConfig().lang;
42
+ }