@solongate/proxy 0.59.4 → 0.60.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.
Files changed (40) hide show
  1. package/dist/api-client/agents.d.ts +10 -0
  2. package/dist/api-client/audit.d.ts +22 -0
  3. package/dist/api-client/client.d.ts +39 -0
  4. package/dist/api-client/index.d.ts +18 -0
  5. package/dist/api-client/policies.d.ts +82 -0
  6. package/dist/api-client/settings.d.ts +22 -0
  7. package/dist/api-client/stats.d.ts +9 -0
  8. package/dist/api-client/types.d.ts +224 -0
  9. package/dist/audit/index.js +0 -1
  10. package/dist/commands/agents.d.ts +4 -0
  11. package/dist/commands/args.d.ts +15 -0
  12. package/dist/commands/audit.d.ts +1 -0
  13. package/dist/commands/dlp.d.ts +1 -0
  14. package/dist/commands/format.d.ts +22 -0
  15. package/dist/commands/index.d.ts +7 -0
  16. package/dist/commands/index.js +938 -0
  17. package/dist/commands/policy.d.ts +1 -0
  18. package/dist/commands/ratelimit.d.ts +1 -0
  19. package/dist/commands/stats.d.ts +1 -0
  20. package/dist/index.js +2059 -348
  21. package/dist/inject.js +4 -4
  22. package/dist/lib.js +3 -31
  23. package/dist/proxy.d.ts +0 -7
  24. package/dist/pull-push.js +2 -1
  25. package/dist/tui/App.d.ts +1 -0
  26. package/dist/tui/components.d.ts +40 -0
  27. package/dist/tui/hooks.d.ts +13 -0
  28. package/dist/tui/index.d.ts +1 -0
  29. package/dist/tui/index.js +881 -0
  30. package/dist/tui/panels/Agents.d.ts +3 -0
  31. package/dist/tui/panels/Audit.d.ts +4 -0
  32. package/dist/tui/panels/Dlp.d.ts +4 -0
  33. package/dist/tui/panels/Policies.d.ts +4 -0
  34. package/dist/tui/panels/RateLimit.d.ts +4 -0
  35. package/dist/tui/panels/Stats.d.ts +3 -0
  36. package/dist/tui/theme.d.ts +16 -0
  37. package/hooks/audit.mjs +23 -4
  38. package/hooks/guard.bundled.mjs +46 -35
  39. package/hooks/guard.mjs +16 -2
  40. package/package.json +7 -2
@@ -0,0 +1,881 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/tui/index.tsx
8
+ import { render } from "ink";
9
+
10
+ // src/tui/App.tsx
11
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
12
+ import { useState as useState6 } from "react";
13
+
14
+ // src/cli-utils.ts
15
+ var c = {
16
+ reset: "\x1B[0m",
17
+ bold: "\x1B[1m",
18
+ dim: "\x1B[2m",
19
+ italic: "\x1B[3m",
20
+ white: "\x1B[97m",
21
+ gray: "\x1B[90m",
22
+ blue1: "\x1B[38;2;20;50;160m",
23
+ blue2: "\x1B[38;2;40;80;190m",
24
+ blue3: "\x1B[38;2;60;110;215m",
25
+ blue4: "\x1B[38;2;90;140;230m",
26
+ blue5: "\x1B[38;2;130;170;240m",
27
+ blue6: "\x1B[38;2;170;200;250m",
28
+ green: "\x1B[38;2;80;200;120m",
29
+ red: "\x1B[38;2;220;80;80m",
30
+ cyan: "\x1B[38;2;100;200;220m",
31
+ yellow: "\x1B[38;2;220;200;80m",
32
+ bgBlue: "\x1B[48;2;20;50;160m"
33
+ };
34
+ var BANNER_FULL = [
35
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
36
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
37
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 ",
38
+ " \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D ",
39
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
40
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
41
+ ];
42
+ var BANNER_COLORS = [c.blue1, c.blue2, c.blue3, c.blue4, c.blue5, c.blue6];
43
+
44
+ // src/tui/components.tsx
45
+ import { Box, Text } from "ink";
46
+
47
+ // src/tui/theme.ts
48
+ var theme = {
49
+ accent: "cyan",
50
+ accentBright: "#5a8ce6",
51
+ ok: "green",
52
+ warn: "yellow",
53
+ bad: "red",
54
+ dim: "gray"
55
+ };
56
+ function decisionColor(decision) {
57
+ const d = (decision || "").toUpperCase();
58
+ if (d === "ALLOW") return theme.ok;
59
+ if (d === "DENY" || d === "DENIED") return theme.bad;
60
+ return theme.dim;
61
+ }
62
+ function modeColor(m) {
63
+ if (m === "block" || m === "active" || m === "on") return theme.ok;
64
+ if (m === "detect" || m === "idle") return theme.warn;
65
+ return theme.dim;
66
+ }
67
+ var BLOCKS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
68
+ function sparkline(values, width) {
69
+ let v = values;
70
+ if (width && v.length > width) v = v.slice(v.length - width);
71
+ if (v.length === 0) return "";
72
+ const max = Math.max(...v, 0);
73
+ if (max === 0) return BLOCKS[0].repeat(v.length);
74
+ return v.map((n) => BLOCKS[Math.min(BLOCKS.length - 1, Math.round(n / max * (BLOCKS.length - 1)))]).join("");
75
+ }
76
+ function truncate(s, n) {
77
+ if (!s) return "";
78
+ return s.length <= n ? s : s.slice(0, Math.max(0, n - 1)) + "\u2026";
79
+ }
80
+ function ago(ts) {
81
+ const t = typeof ts === "number" ? ts : Date.parse(ts);
82
+ if (Number.isNaN(t)) return String(ts ?? "");
83
+ const s = Math.max(0, (Date.now() - t) / 1e3);
84
+ if (s < 60) return `${Math.floor(s)}s`;
85
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
86
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
87
+ return `${Math.floor(s / 86400)}d`;
88
+ }
89
+
90
+ // src/tui/components.tsx
91
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
92
+ function DataView({
93
+ loading,
94
+ error,
95
+ empty,
96
+ emptyText,
97
+ children
98
+ }) {
99
+ if (error) return /* @__PURE__ */ jsxs(Text, { color: theme.bad, children: [
100
+ "\u2717 ",
101
+ error
102
+ ] });
103
+ if (loading) return /* @__PURE__ */ jsx(Text, { color: theme.dim, children: "Loading\u2026" });
104
+ if (empty) return /* @__PURE__ */ jsx(Text, { color: theme.dim, children: emptyText ?? "No data." });
105
+ return /* @__PURE__ */ jsx(Fragment, { children });
106
+ }
107
+ function Table({ columns, rows }) {
108
+ const fit = (s, w) => {
109
+ const v = s ?? "";
110
+ if (v.length > w) return v.slice(0, Math.max(0, w - 1)) + "\u2026";
111
+ return v.padEnd(w);
112
+ };
113
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
114
+ /* @__PURE__ */ jsx(Box, { children: columns.map((c2, i) => /* @__PURE__ */ jsx(Text, { color: theme.dim, children: fit(c2.header, c2.width) + " " }, i)) }),
115
+ rows.map((row, ri) => /* @__PURE__ */ jsx(Box, { children: row.map((cell, ci) => /* @__PURE__ */ jsx(Text, { color: cell.color, dimColor: cell.dim, bold: cell.bold, children: fit(cell.value, columns[ci]?.width ?? 10) + " " }, ci)) }, ri))
116
+ ] });
117
+ }
118
+ function KeyHints({ hints }) {
119
+ return /* @__PURE__ */ jsx(Box, { children: hints.map(([key, label], i) => /* @__PURE__ */ jsxs(Text, { color: theme.dim, children: [
120
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: key }),
121
+ " ",
122
+ label,
123
+ i < hints.length - 1 ? " " : ""
124
+ ] }, i)) });
125
+ }
126
+
127
+ // src/tui/panels/Policies.tsx
128
+ import { Box as Box2, Text as Text2, useInput } from "ink";
129
+ import { useState as useState2 } from "react";
130
+
131
+ // src/api-client/client.ts
132
+ import { readFileSync, existsSync } from "fs";
133
+ import { resolve, join } from "path";
134
+ import { homedir } from "os";
135
+ var DEFAULT_API_URL = "https://api.solongate.com";
136
+ var ApiError = class extends Error {
137
+ status;
138
+ code;
139
+ constructor(status, code, message) {
140
+ super(message);
141
+ this.name = "ApiError";
142
+ this.status = status;
143
+ this.code = code;
144
+ }
145
+ };
146
+ var NotAuthenticatedError = class extends Error {
147
+ constructor() {
148
+ super("Not logged in. Run `solongate login` first.");
149
+ this.name = "NotAuthenticatedError";
150
+ }
151
+ };
152
+ function loginCredentialFile() {
153
+ try {
154
+ const p = join(homedir(), ".solongate", "cloud-guard.json");
155
+ if (!existsSync(p)) return {};
156
+ const c2 = JSON.parse(readFileSync(p, "utf-8"));
157
+ return c2 && typeof c2 === "object" ? c2 : {};
158
+ } catch {
159
+ return {};
160
+ }
161
+ }
162
+ function dotenvApiKey() {
163
+ try {
164
+ const envPath = resolve(".env");
165
+ if (!existsSync(envPath)) return void 0;
166
+ for (const line of readFileSync(envPath, "utf-8").split("\n")) {
167
+ const trimmed = line.trim();
168
+ if (!trimmed || trimmed.startsWith("#")) continue;
169
+ const eq = trimmed.indexOf("=");
170
+ if (eq === -1) continue;
171
+ const key = trimmed.slice(0, eq).trim();
172
+ if (key !== "SOLONGATE_API_KEY") continue;
173
+ return trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
174
+ }
175
+ } catch {
176
+ }
177
+ return void 0;
178
+ }
179
+ var cached = null;
180
+ function isAuthenticated() {
181
+ try {
182
+ resolveCredentials();
183
+ return true;
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+ function resolveCredentials(apiUrlOverride) {
189
+ if (cached && !apiUrlOverride) return cached;
190
+ const file = loginCredentialFile();
191
+ const apiKey = process.env["SOLONGATE_API_KEY"] || file.apiKey || dotenvApiKey();
192
+ if (!apiKey) throw new NotAuthenticatedError();
193
+ const apiUrl = apiUrlOverride || process.env["SOLONGATE_API_URL"] || file.apiUrl || DEFAULT_API_URL;
194
+ const creds = { apiKey, apiUrl: apiUrl.replace(/\/$/, "") };
195
+ if (!apiUrlOverride) cached = creds;
196
+ return creds;
197
+ }
198
+ function buildUrl(base, path, query) {
199
+ const url = new URL(`${base}/api/v1${path}`);
200
+ if (query) {
201
+ for (const [k, v] of Object.entries(query)) {
202
+ if (v === void 0) continue;
203
+ url.searchParams.set(k, String(v));
204
+ }
205
+ }
206
+ return url.toString();
207
+ }
208
+ async function request(method, path, opts = {}) {
209
+ const creds = resolveCredentials(opts.apiUrl);
210
+ const url = buildUrl(creds.apiUrl, path, opts.query);
211
+ const headers = {
212
+ Authorization: `Bearer ${creds.apiKey}`
213
+ };
214
+ let bodyInit;
215
+ if (opts.body !== void 0) {
216
+ headers["Content-Type"] = "application/json";
217
+ bodyInit = JSON.stringify(opts.body);
218
+ }
219
+ let res;
220
+ try {
221
+ res = await fetch(url, {
222
+ method,
223
+ headers,
224
+ body: bodyInit,
225
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
226
+ });
227
+ } catch (err) {
228
+ const msg = err instanceof Error ? err.message : String(err);
229
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
230
+ }
231
+ const text = await res.text().catch(() => "");
232
+ let json = void 0;
233
+ if (text) {
234
+ try {
235
+ json = JSON.parse(text);
236
+ } catch {
237
+ }
238
+ }
239
+ if (!res.ok) {
240
+ const envelope = json?.error;
241
+ if (envelope && typeof envelope === "object") {
242
+ throw new ApiError(res.status, envelope.code || "ERROR", envelope.message || res.statusText);
243
+ }
244
+ if (typeof envelope === "string") {
245
+ throw new ApiError(res.status, "ERROR", envelope);
246
+ }
247
+ if (res.status === 401) throw new ApiError(401, "AUTHENTICATION_ERROR", "Invalid API key. Run `solongate login`.");
248
+ if (res.status === 429) throw new ApiError(429, "RATE_LIMITED", "Rate limited by the API. Slow down and retry.");
249
+ throw new ApiError(res.status, "ERROR", text || res.statusText || `HTTP ${res.status}`);
250
+ }
251
+ return json;
252
+ }
253
+
254
+ // src/api-client/policies.ts
255
+ var policies_exports = {};
256
+ __export(policies_exports, {
257
+ active: () => active,
258
+ addRule: () => addRule,
259
+ create: () => create,
260
+ dryRun: () => dryRun,
261
+ get: () => get,
262
+ list: () => list,
263
+ remove: () => remove,
264
+ revokeRule: () => revokeRule,
265
+ rollback: () => rollback,
266
+ setActive: () => setActive,
267
+ update: () => update,
268
+ versions: () => versions
269
+ });
270
+ function list() {
271
+ return request("GET", "/policies");
272
+ }
273
+ function get(id, version) {
274
+ return request("GET", `/policies/${encodeURIComponent(id)}`, {
275
+ query: version !== void 0 ? { version } : void 0
276
+ });
277
+ }
278
+ function create(policy) {
279
+ return request("POST", "/policies", { body: policy });
280
+ }
281
+ function update(id, policy) {
282
+ return request("PUT", `/policies/${encodeURIComponent(id)}`, { body: policy });
283
+ }
284
+ function remove(id) {
285
+ return request("DELETE", `/policies/${encodeURIComponent(id)}`);
286
+ }
287
+ function addRule(id, spec) {
288
+ return request("POST", `/policies/${encodeURIComponent(id)}/rules`, { body: spec });
289
+ }
290
+ function revokeRule(id, ruleId) {
291
+ return request("DELETE", `/policies/${encodeURIComponent(id)}/rules/${encodeURIComponent(ruleId)}`);
292
+ }
293
+ function versions(id, opts = {}) {
294
+ return request("GET", `/policies/${encodeURIComponent(id)}/versions`, { query: opts });
295
+ }
296
+ function rollback(id, version) {
297
+ return request("POST", `/policies/${encodeURIComponent(id)}/rollback`, { body: { version } });
298
+ }
299
+ function active(agentId) {
300
+ return request("GET", "/policies/active", { query: agentId ? { agent_id: agentId } : void 0 });
301
+ }
302
+ function setActive(policyId) {
303
+ return request("POST", "/policies/active", { body: { policyId: policyId ?? "" } });
304
+ }
305
+ function dryRun(body) {
306
+ return request("POST", "/policies/dry-run", { body });
307
+ }
308
+
309
+ // src/api-client/settings.ts
310
+ var settings_exports = {};
311
+ __export(settings_exports, {
312
+ clearRateLimitHistory: () => clearRateLimitHistory,
313
+ getGuardStatus: () => getGuardStatus,
314
+ getRateLimitHistory: () => getRateLimitHistory,
315
+ getSecurityLayers: () => getSecurityLayers,
316
+ setSecurityLayers: () => setSecurityLayers
317
+ });
318
+ function getSecurityLayers() {
319
+ return request("GET", "/settings/security-layers");
320
+ }
321
+ function setSecurityLayers(layers) {
322
+ return request("PUT", "/settings/security-layers", { body: { layers } });
323
+ }
324
+ function getRateLimitHistory() {
325
+ return request("GET", "/settings/rate-limit-history");
326
+ }
327
+ function clearRateLimitHistory() {
328
+ return request("DELETE", "/settings/rate-limit-history", { query: { all: 1 } });
329
+ }
330
+ function getGuardStatus() {
331
+ return request("GET", "/settings/guard-status");
332
+ }
333
+
334
+ // src/api-client/stats.ts
335
+ var stats_exports = {};
336
+ __export(stats_exports, {
337
+ drift: () => drift,
338
+ get: () => get2,
339
+ securityInsights: () => securityInsights,
340
+ timeseries: () => timeseries
341
+ });
342
+ function get2() {
343
+ return request("GET", "/stats");
344
+ }
345
+ function timeseries(opts = {}) {
346
+ return request("GET", "/stats/timeseries", { query: opts });
347
+ }
348
+ function drift(days) {
349
+ return request("GET", "/stats/drift", { query: days !== void 0 ? { days } : void 0 });
350
+ }
351
+ function securityInsights(days) {
352
+ return request("GET", "/stats/security-insights", { query: days !== void 0 ? { days } : void 0 });
353
+ }
354
+
355
+ // src/api-client/audit.ts
356
+ var audit_exports = {};
357
+ __export(audit_exports, {
358
+ list: () => list2,
359
+ whitelist: () => whitelist
360
+ });
361
+ function list2(query = {}) {
362
+ return request("GET", "/audit-logs", { query });
363
+ }
364
+ function whitelist(id, scope = "exact") {
365
+ return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
366
+ }
367
+
368
+ // src/api-client/agents.ts
369
+ var agents_exports = {};
370
+ __export(agents_exports, {
371
+ anomalies: () => anomalies,
372
+ get: () => get3,
373
+ live: () => live
374
+ });
375
+ function live(opts = {}) {
376
+ return request("GET", "/agents/live", {
377
+ query: { limit: opts.limit, include_deactivated: opts.includeDeactivated ? 1 : void 0 }
378
+ });
379
+ }
380
+ function get3(id, scan = false) {
381
+ return request("GET", `/agents/${encodeURIComponent(id)}`, { query: scan ? { scan: 1 } : void 0 });
382
+ }
383
+ function anomalies(id, limit) {
384
+ return request("GET", `/agents/${encodeURIComponent(id)}/anomalies`, {
385
+ query: limit !== void 0 ? { limit } : void 0
386
+ });
387
+ }
388
+
389
+ // src/api-client/index.ts
390
+ var api = { policies: policies_exports, settings: settings_exports, stats: stats_exports, audit: audit_exports, agents: agents_exports };
391
+
392
+ // src/tui/hooks.ts
393
+ import { useCallback, useEffect, useRef, useState } from "react";
394
+ function useLoader(fn, deps = []) {
395
+ const [data, setData] = useState(null);
396
+ const [error, setError] = useState(null);
397
+ const [loading, setLoading] = useState(true);
398
+ const [nonce, setNonce] = useState(0);
399
+ const alive = useRef(true);
400
+ const fnRef = useRef(fn);
401
+ fnRef.current = fn;
402
+ useEffect(() => {
403
+ alive.current = true;
404
+ return () => {
405
+ alive.current = false;
406
+ };
407
+ }, []);
408
+ useEffect(() => {
409
+ setLoading(true);
410
+ fnRef.current().then((d) => {
411
+ if (!alive.current) return;
412
+ setData(d);
413
+ setError(null);
414
+ }).catch((e) => {
415
+ if (!alive.current) return;
416
+ setError(e instanceof Error ? e.message : String(e));
417
+ }).finally(() => {
418
+ if (alive.current) setLoading(false);
419
+ });
420
+ }, [nonce, ...deps]);
421
+ const reload = useCallback(() => setNonce((n) => n + 1), []);
422
+ return { data, error, loading, reload };
423
+ }
424
+ function usePoll(reload, intervalMs, enabled = true) {
425
+ useEffect(() => {
426
+ if (!enabled) return;
427
+ const t = setInterval(reload, intervalMs);
428
+ return () => clearInterval(t);
429
+ }, [reload, intervalMs, enabled]);
430
+ }
431
+
432
+ // src/tui/panels/Policies.tsx
433
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
434
+ function PoliciesPanel({ focused }) {
435
+ const [sel, setSel] = useState2(0);
436
+ const list3 = useLoader(() => api.policies.list());
437
+ const policies = list3.data?.policies ?? [];
438
+ const selected = policies[Math.min(sel, Math.max(0, policies.length - 1))];
439
+ const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
440
+ useInput(
441
+ (_input, key) => {
442
+ if (key.upArrow) setSel((n) => Math.max(0, n - 1));
443
+ if (key.downArrow) setSel((n) => Math.min(policies.length - 1, n + 1));
444
+ },
445
+ { isActive: focused }
446
+ );
447
+ return /* @__PURE__ */ jsx2(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
448
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select policy" : "press \u2192 to browse" }),
449
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? theme.accentBright : void 0, bold: i === sel, children: [
450
+ (i === sel ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
451
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
452
+ p.mode.padEnd(10),
453
+ " ",
454
+ p.rules.length,
455
+ " rules \xB7 v",
456
+ p.version
457
+ ] })
458
+ ] }, p.id)) }),
459
+ selected ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
460
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
461
+ "Rules of ",
462
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: selected.id })
463
+ ] }),
464
+ /* @__PURE__ */ jsx2(
465
+ Table,
466
+ {
467
+ columns: [
468
+ { header: "EFFECT", width: 7 },
469
+ { header: "PRIO", width: 4 },
470
+ { header: "TOOL", width: 22 },
471
+ { header: "DESCRIPTION", width: 34 }
472
+ ],
473
+ rows: (detail.data?.rules ?? []).slice(0, 10).map((r) => [
474
+ { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad },
475
+ { value: String(r.priority), dim: true },
476
+ { value: truncate(r.toolPattern, 22), color: theme.accent },
477
+ { value: truncate(r.description || "\u2014", 34) }
478
+ ])
479
+ }
480
+ )
481
+ ] }) : null
482
+ ] }) });
483
+ }
484
+
485
+ // src/tui/panels/RateLimit.tsx
486
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
487
+ import { useEffect as useEffect2, useState as useState3 } from "react";
488
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
489
+ var MODES = ["off", "detect", "block"];
490
+ var FIELDS = ["mode", "perMinute", "perHour", "perDay"];
491
+ function RateLimitPanel({ focused }) {
492
+ const layersQ = useLoader(() => api.settings.getSecurityLayers());
493
+ const historyQ = useLoader(() => api.settings.getRateLimitHistory());
494
+ const [draft, setDraft] = useState3(null);
495
+ const [dirty, setDirty] = useState3(false);
496
+ const [fi, setFi] = useState3(0);
497
+ const [status, setStatus] = useState3(null);
498
+ useEffect2(() => {
499
+ if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
500
+ }, [layersQ.data, draft]);
501
+ const adjust = (dir) => {
502
+ if (!draft) return;
503
+ const field = FIELDS[fi];
504
+ if (field === "mode") {
505
+ const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
506
+ setDraft({ ...draft, mode: MODES[idx] });
507
+ } else {
508
+ const step = 10;
509
+ setDraft({ ...draft, [field]: Math.max(0, draft[field] + dir * step) });
510
+ }
511
+ setDirty(true);
512
+ setStatus(null);
513
+ };
514
+ const save = async () => {
515
+ if (!draft || !layersQ.data) return;
516
+ setStatus("Saving\u2026");
517
+ try {
518
+ const next = { ...layersQ.data.layers, rateLimit: draft };
519
+ const res = await api.settings.setSecurityLayers(next);
520
+ setDraft({ ...res.layers.rateLimit });
521
+ setDirty(false);
522
+ setStatus("\u2713 Saved");
523
+ historyQ.reload();
524
+ } catch (e) {
525
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
526
+ }
527
+ };
528
+ useInput2(
529
+ (input, key) => {
530
+ if (key.upArrow) setFi((n) => (n - 1 + FIELDS.length) % FIELDS.length);
531
+ else if (key.downArrow) setFi((n) => (n + 1) % FIELDS.length);
532
+ else if (key.leftArrow) adjust(-1);
533
+ else if (key.rightArrow) adjust(1);
534
+ else if (input === "s") void save();
535
+ },
536
+ { isActive: focused }
537
+ );
538
+ const history = historyQ.data?.history ?? [];
539
+ return /* @__PURE__ */ jsx3(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
540
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \u2190\u2192 change s save" : "press \u2192 to edit" }),
541
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
542
+ /* @__PURE__ */ jsx3(Row, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
543
+ /* @__PURE__ */ jsx3(Row, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
544
+ /* @__PURE__ */ jsx3(Row, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
545
+ /* @__PURE__ */ jsx3(Row, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
546
+ ] }),
547
+ history.length ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
548
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "history " }),
549
+ /* @__PURE__ */ jsx3(Text3, { color: theme.accent, children: sparkline(history.map((h) => h.minute), 40) }),
550
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " " + history.length + " changes" })
551
+ ] }) : null,
552
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
553
+ dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
554
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
555
+ ] })
556
+ ] }) : null });
557
+ }
558
+ function Row({ label, active: active2, children }) {
559
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
560
+ /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
561
+ children
562
+ ] });
563
+ }
564
+
565
+ // src/tui/panels/Dlp.tsx
566
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
567
+ import { useEffect as useEffect3, useState as useState4 } from "react";
568
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
569
+ var MODES2 = ["off", "detect", "block"];
570
+ function DlpPanel({ focused }) {
571
+ const q = useLoader(() => api.settings.getSecurityLayers());
572
+ const [dlp, setDlp] = useState4(null);
573
+ const [available, setAvailable] = useState4([]);
574
+ const [sel, setSel] = useState4(0);
575
+ const [status, setStatus] = useState4(null);
576
+ useEffect3(() => {
577
+ if (q.data && !dlp) {
578
+ setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns] });
579
+ setAvailable(q.data.availablePatterns);
580
+ }
581
+ }, [q.data, dlp]);
582
+ const persist = async (nextDlp) => {
583
+ if (!q.data) return;
584
+ setDlp(nextDlp);
585
+ setStatus("Saving\u2026");
586
+ try {
587
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp: nextDlp });
588
+ setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns] });
589
+ setStatus("\u2713 Saved");
590
+ } catch (e) {
591
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
592
+ }
593
+ };
594
+ useInput3(
595
+ (input, key) => {
596
+ if (!dlp) return;
597
+ if (key.upArrow) setSel((n) => Math.max(0, n - 1));
598
+ else if (key.downArrow) setSel((n) => Math.min(available.length - 1, n + 1));
599
+ else if (input === " " || key.return) {
600
+ const p = available[sel];
601
+ if (!p) return;
602
+ const set = new Set(dlp.patterns);
603
+ if (set.has(p)) set.delete(p);
604
+ else set.add(p);
605
+ void persist({ ...dlp, patterns: [...set] });
606
+ } else if (input === "m") {
607
+ const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
608
+ void persist({ ...dlp, mode: MODES2[idx] });
609
+ }
610
+ },
611
+ { isActive: focused }
612
+ );
613
+ const enabled = new Set(dlp?.patterns ?? []);
614
+ return /* @__PURE__ */ jsx4(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
615
+ /* @__PURE__ */ jsxs4(Box4, { children: [
616
+ /* @__PURE__ */ jsx4(Text4, { children: "mode: " }),
617
+ /* @__PURE__ */ jsx4(Text4, { color: modeColor(dlp.mode), children: dlp.mode }),
618
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? " \u2191\u2193 move \xB7 space toggle \xB7 m mode" : " press \u2192 to edit" })
619
+ ] }),
620
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
621
+ const on = enabled.has(p);
622
+ return /* @__PURE__ */ jsxs4(Text4, { color: focused && i === sel ? theme.accentBright : void 0, bold: focused && i === sel, children: [
623
+ (focused && i === sel ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
624
+ /* @__PURE__ */ jsx4(Text4, { color: on ? theme.ok : theme.dim, children: p })
625
+ ] }, p);
626
+ }) }),
627
+ dlp.custom.length ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
628
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Custom:" }),
629
+ dlp.custom.map((c2) => /* @__PURE__ */ jsxs4(Text4, { children: [
630
+ " ",
631
+ /* @__PURE__ */ jsx4(Text4, { color: theme.accent, children: c2.name }),
632
+ " ",
633
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: c2.re })
634
+ ] }, c2.name))
635
+ ] }) : null,
636
+ status ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
637
+ ] }) : null });
638
+ }
639
+
640
+ // src/tui/panels/Stats.tsx
641
+ import { Box as Box5, Text as Text5 } from "ink";
642
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
643
+ function StatsPanel({ active: active2 }) {
644
+ const stats = useLoader(() => api.stats.get());
645
+ const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
646
+ usePoll(() => {
647
+ stats.reload();
648
+ ts.reload();
649
+ }, 5e3, active2);
650
+ const s = stats.data;
651
+ const points = ts.data?.timeseries ?? [];
652
+ return /* @__PURE__ */ jsx5(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
653
+ /* @__PURE__ */ jsxs5(Box5, { children: [
654
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
655
+ s.total_calls,
656
+ " "
657
+ ] }),
658
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "calls " }),
659
+ /* @__PURE__ */ jsxs5(Text5, { color: theme.ok, children: [
660
+ s.allowed,
661
+ " allowed"
662
+ ] }),
663
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " " }),
664
+ /* @__PURE__ */ jsxs5(Text5, { color: theme.bad, children: [
665
+ s.denied,
666
+ " denied"
667
+ ] })
668
+ ] }),
669
+ /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
670
+ s.active_policies,
671
+ " policies \xB7 ",
672
+ s.registered_tools,
673
+ " tools"
674
+ ] }),
675
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
676
+ /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
677
+ "Last 24h ",
678
+ ts.data ? `(per ${ts.data.granularity})` : ""
679
+ ] }),
680
+ /* @__PURE__ */ jsxs5(Box5, { children: [
681
+ /* @__PURE__ */ jsx5(Text5, { children: "total " }),
682
+ /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
683
+ ] }),
684
+ /* @__PURE__ */ jsxs5(Box5, { children: [
685
+ /* @__PURE__ */ jsx5(Text5, { children: "allowed " }),
686
+ /* @__PURE__ */ jsx5(Text5, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
687
+ ] }),
688
+ /* @__PURE__ */ jsxs5(Box5, { children: [
689
+ /* @__PURE__ */ jsx5(Text5, { children: "denied " }),
690
+ /* @__PURE__ */ jsx5(Text5, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
691
+ ] })
692
+ ] }),
693
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
694
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Recent" }),
695
+ /* @__PURE__ */ jsx5(
696
+ Table,
697
+ {
698
+ columns: [
699
+ { header: "DECISION", width: 9 },
700
+ { header: "TOOL", width: 20 },
701
+ { header: "MS", width: 5 },
702
+ { header: "WHEN", width: 6 }
703
+ ],
704
+ rows: (s.recent_activity ?? []).slice(0, 8).map((a) => [
705
+ { value: a.decision, color: decisionColor(a.decision) },
706
+ { value: truncate(a.tool_name, 20), color: theme.accent },
707
+ { value: String(a.evaluation_time_ms ?? "\u2014"), dim: true },
708
+ { value: ago(a.created_at), dim: true }
709
+ ])
710
+ }
711
+ )
712
+ ] })
713
+ ] }) : null });
714
+ }
715
+
716
+ // src/tui/panels/Audit.tsx
717
+ import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
718
+ import { useState as useState5 } from "react";
719
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
720
+ var FILTERS = [void 0, "DENY", "ALLOW"];
721
+ function AuditPanel({ active: active2, focused }) {
722
+ const [fi, setFi] = useState5(0);
723
+ const filter = FILTERS[fi];
724
+ const audit = useLoader(() => api.audit.list({ filter, limit: 14 }), [fi]);
725
+ usePoll(audit.reload, 5e3, active2);
726
+ useInput4(
727
+ (input) => {
728
+ if (input === "f") setFi((n) => (n + 1) % FILTERS.length);
729
+ },
730
+ { isActive: focused }
731
+ );
732
+ const entries = audit.data?.entries ?? [];
733
+ return /* @__PURE__ */ jsx6(DataView, { loading: audit.loading && !audit.data, error: audit.error, empty: !!audit.data && entries.length === 0, emptyText: "No audit entries.", children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
734
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
735
+ audit.data?.total ?? 0,
736
+ " entries \xB7 filter: ",
737
+ /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: filter ?? "all" }),
738
+ " (press f)"
739
+ ] }),
740
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(
741
+ Table,
742
+ {
743
+ columns: [
744
+ { header: "DECISION", width: 9 },
745
+ { header: "TOOL", width: 18 },
746
+ { header: "AGENT", width: 14 },
747
+ { header: "REASON", width: 26 },
748
+ { header: "DLP", width: 4 },
749
+ { header: "WHEN", width: 6 }
750
+ ],
751
+ rows: entries.map((e) => [
752
+ { value: e.decision, color: decisionColor(e.decision) },
753
+ { value: truncate(e.tool_name, 18), color: theme.accent },
754
+ { value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
755
+ { value: truncate(e.reason ?? "\u2014", 26) },
756
+ { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
757
+ { value: ago(e.created_at), dim: true }
758
+ ])
759
+ }
760
+ ) })
761
+ ] }) });
762
+ }
763
+
764
+ // src/tui/panels/Agents.tsx
765
+ import { Box as Box7, Text as Text7 } from "ink";
766
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
767
+ function AgentsPanel({ active: active2 }) {
768
+ const agents = useLoader(() => api.agents.live({ limit: 14 }));
769
+ usePoll(agents.reload, 4e3, active2);
770
+ const data = agents.data;
771
+ const rows = data?.agents ?? [];
772
+ return /* @__PURE__ */ jsx7(DataView, { loading: agents.loading && !data, error: agents.error, empty: !!data && rows.length === 0, emptyText: "No agent sessions.", children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
773
+ data ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
774
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.ok, children: [
775
+ data.counts.active,
776
+ " active"
777
+ ] }),
778
+ " \xB7 ",
779
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
780
+ data.counts.idle,
781
+ " idle"
782
+ ] }),
783
+ " \xB7 ",
784
+ data.counts.deactivated,
785
+ " off"
786
+ ] }) : null,
787
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
788
+ Table,
789
+ {
790
+ columns: [
791
+ { header: "STATUS", width: 7 },
792
+ { header: "AGENT", width: 20 },
793
+ { header: "CALLS", width: 6 },
794
+ { header: "DENY", width: 5 },
795
+ { header: "DLP", width: 4 },
796
+ { header: "TRUST", width: 6 },
797
+ { header: "CHARACTER", width: 20 }
798
+ ],
799
+ rows: rows.map((a) => [
800
+ { value: a.status, color: modeColor(a.status) },
801
+ { value: truncate(a.agent_name ?? a.agent_id ?? a.session_id, 20), color: theme.accent },
802
+ { value: String(a.total_calls) },
803
+ { value: String(a.denied_calls), color: a.denied_calls ? theme.bad : void 0, dim: !a.denied_calls },
804
+ { value: String(a.dlp_events), color: a.dlp_events ? theme.bad : void 0, dim: !a.dlp_events },
805
+ { value: `${a.trust_score}` },
806
+ { value: truncate(a.character || "\u2014", 20), dim: true }
807
+ ])
808
+ }
809
+ ) })
810
+ ] }) });
811
+ }
812
+
813
+ // src/tui/App.tsx
814
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
815
+ var SECTIONS = [
816
+ { label: "Policies", Panel: PoliciesPanel },
817
+ { label: "Rate Limit", Panel: RateLimitPanel },
818
+ { label: "DLP", Panel: DlpPanel },
819
+ { label: "Stats", Panel: StatsPanel },
820
+ { label: "Audit", Panel: AuditPanel },
821
+ { label: "Agents", Panel: AgentsPanel }
822
+ ];
823
+ var BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8FA"];
824
+ function Banner() {
825
+ const wide = (process.stdout.columns ?? 80) >= 82;
826
+ if (!wide) {
827
+ return /* @__PURE__ */ jsxs8(Box8, { children: [
828
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accentBright, children: "SolonGate" }),
829
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " \u2014 security control center" })
830
+ ] });
831
+ }
832
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
833
+ BANNER_FULL.map((line, i) => /* @__PURE__ */ jsx8(Text8, { bold: true, color: BANNER_HEX[i], children: line }, i)),
834
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " security control center \xB7 manage policies, rate limits, DLP & more" })
835
+ ] });
836
+ }
837
+ function App() {
838
+ const { exit } = useApp();
839
+ const [section, setSection] = useState6(0);
840
+ const [focus, setFocus] = useState6("nav");
841
+ useInput5((input, key) => {
842
+ if (focus === "nav") {
843
+ if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
844
+ else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
845
+ else if (key.rightArrow || key.return || key.tab) setFocus("panel");
846
+ else if (input === "q") exit();
847
+ } else {
848
+ if (key.escape || key.tab) setFocus("nav");
849
+ }
850
+ });
851
+ const current = SECTIONS[section];
852
+ const Panel = current.Panel;
853
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
854
+ /* @__PURE__ */ jsx8(Banner, {}),
855
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
856
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === section ? theme.accentBright : void 0, bold: i === section, children: (i === section ? "\u25B8 " : " ") + s.label }, s.label)) }),
857
+ /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, minHeight: 16, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: true, focused: focus === "panel" }) })
858
+ ] }),
859
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "move"], ["\u2190\u2192/space/s", "edit"], ["esc", "back to menu"]] }) })
860
+ ] });
861
+ }
862
+
863
+ // src/tui/index.tsx
864
+ import { jsx as jsx9 } from "react/jsx-runtime";
865
+ async function launchTui() {
866
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
867
+ process.stderr.write(
868
+ "The SolonGate TUI needs an interactive terminal.\nUse the scriptable commands instead, e.g. `solongate stats`, `solongate policy list`.\n"
869
+ );
870
+ return;
871
+ }
872
+ if (!isAuthenticated()) {
873
+ process.stderr.write("Not logged in. Run `solongate login` first.\n");
874
+ return;
875
+ }
876
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, {}));
877
+ await waitUntilExit();
878
+ }
879
+ export {
880
+ launchTui
881
+ };