@hoox-sh/hoox-tui 0.2.0 → 0.3.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/README.md +50 -31
- package/dist/main.js +4238 -1711
- package/package.json +2 -2
- package/src/app.tsx +45 -30
- package/src/cli-bridge-test-double.ts +48 -1
- package/src/components/layout/sidebar.tsx +27 -11
- package/src/components/layout/statusbar.test.tsx +19 -5
- package/src/components/layout/statusbar.tsx +34 -18
- package/src/components/shared/crash-screen.tsx +24 -2
- package/src/components/shared/error-boundary.tsx +56 -33
- package/src/components/ui/connection-toasts.test.ts +58 -1
- package/src/components/views/config-editor.test.tsx +4 -2
- package/src/components/views/config-editor.tsx +31 -3
- package/src/components/views/dashboard/alerts-panel.tsx +36 -8
- package/src/components/views/dashboard/auto-repair-helpers.test.ts +23 -0
- package/src/components/views/dashboard.test.tsx +2 -1
- package/src/components/views/dashboard.tsx +126 -41
- package/src/components/views/db-query.test.tsx +7 -0
- package/src/components/views/db-query.tsx +5 -4
- package/src/components/views/edge-topology.tsx +3 -2
- package/src/components/views/kv-viewer.test.tsx +28 -0
- package/src/components/views/kv-viewer.tsx +4 -3
- package/src/components/views/logs-viewer.tsx +46 -19
- package/src/components/views/queue-depth.test.tsx +39 -0
- package/src/components/views/queue-depth.tsx +3 -2
- package/src/components/views/secrets-viewer.test.tsx +2 -2
- package/src/components/views/secrets-viewer.tsx +4 -3
- package/src/components/views/service-manager.tsx +147 -53
- package/src/components/views/settings.tsx +50 -10
- package/src/components/views/setup-wizard.test.tsx +13 -0
- package/src/components/views/setup-wizard.tsx +53 -22
- package/src/components/views/trade-monitor.tsx +20 -13
- package/src/components/views/worker-detail.tsx +27 -12
- package/src/components/views/worker-settings.test.tsx +5 -6
- package/src/components/views/worker-settings.tsx +13 -11
- package/src/components/views/workers-overview.tsx +27 -6
- package/src/hooks/hooks.test.ts +303 -9
- package/src/hooks/index.ts +7 -1
- package/src/hooks/shell-overlay.ts +41 -0
- package/src/hooks/use-polling.ts +91 -35
- package/src/hooks/use-service-data.ts +21 -4
- package/src/main.tsx +15 -0
- package/src/services/cli-bridge/index.ts +28 -22
- package/src/services/cli-bridge/standalone.ts +29 -2
- package/src/services/cli-bridge.test.ts +848 -1
- package/src/services/clipboard.test.ts +217 -3
- package/src/services/dashboard-settings-loader.test.ts +50 -0
- package/src/services/tui-connection.test.ts +21 -0
- package/src/stores/service-store.test.ts +89 -3
- package/src/stores/ui-store.test.ts +23 -0
- package/src/test-setup.ts +11 -2
- package/src/types.ts +1 -0
- package/src/view-registry.test.ts +3 -1
- package/src/view-registry.tsx +14 -1
|
@@ -178,7 +178,8 @@ function killProc(
|
|
|
178
178
|
* [ { "results": [ {...}, {...} ], "success": true,
|
|
179
179
|
* "meta": { "duration": 0.123, "rows_read": 42, ... } } ]
|
|
180
180
|
*/
|
|
181
|
-
|
|
181
|
+
/** Exported for unit tests — pure stdout → DbQueryResult normalizer. */
|
|
182
|
+
export function parseDbQueryResult(stdout: string): DbQueryResult {
|
|
182
183
|
const cleaned = stdout.trim();
|
|
183
184
|
if (!cleaned) {
|
|
184
185
|
return {
|
|
@@ -266,7 +267,7 @@ function parseDbQueryResult(stdout: string): DbQueryResult {
|
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
/** Compute the {@link QueueDepthStatus} for a given depth + paused flag. */
|
|
269
|
-
function deriveQueueDepthStatus(
|
|
270
|
+
export function deriveQueueDepthStatus(
|
|
270
271
|
depth: number,
|
|
271
272
|
paused: boolean
|
|
272
273
|
): QueueDepthStatus {
|
|
@@ -285,7 +286,8 @@ function deriveQueueDepthStatus(
|
|
|
285
286
|
* Expected envelope shape:
|
|
286
287
|
* { "queues": [ { "queue_name": "...", "producers_total_count": 2, ... } ] }
|
|
287
288
|
*/
|
|
288
|
-
|
|
289
|
+
/** Exported for unit tests — pure queue-depth envelope parser. */
|
|
290
|
+
export function parseQueueDepths(stdout: string): QueueDepth[] {
|
|
289
291
|
const timestamp = new Date().toISOString();
|
|
290
292
|
const cleaned = stdout.trim();
|
|
291
293
|
if (!cleaned) return [];
|
|
@@ -349,7 +351,8 @@ function parseQueueDepths(stdout: string): QueueDepth[] {
|
|
|
349
351
|
* Values are intentionally omitted — the viewer is strictly read-only.
|
|
350
352
|
* The parser never throws on malformed input.
|
|
351
353
|
*/
|
|
352
|
-
|
|
354
|
+
/** Exported for unit tests — pure secrets-list envelope parser. */
|
|
355
|
+
export function parseSecretsList(stdout: string): SecretMetadata[] {
|
|
353
356
|
const cleaned = stdout.trim();
|
|
354
357
|
if (!cleaned) return [];
|
|
355
358
|
|
|
@@ -357,12 +360,8 @@ function parseSecretsList(stdout: string): SecretMetadata[] {
|
|
|
357
360
|
if (parsed === null) return [];
|
|
358
361
|
|
|
359
362
|
// Single-worker envelope: { worker: string, secrets: string[] }
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
typeof parsed === "object" &&
|
|
363
|
-
"worker" in parsed &&
|
|
364
|
-
"secrets" in parsed
|
|
365
|
-
) {
|
|
363
|
+
// `parsed` is non-null after the early return above.
|
|
364
|
+
if (typeof parsed === "object" && "worker" in parsed && "secrets" in parsed) {
|
|
366
365
|
const envelope = parsed as { worker: string; secrets: unknown };
|
|
367
366
|
if (Array.isArray(envelope.secrets)) {
|
|
368
367
|
const result: SecretMetadata[] = [];
|
|
@@ -381,7 +380,7 @@ function parseSecretsList(stdout: string): SecretMetadata[] {
|
|
|
381
380
|
}
|
|
382
381
|
|
|
383
382
|
// All-workers envelope: Record<string, string[]>
|
|
384
|
-
if (
|
|
383
|
+
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
385
384
|
const entries = Object.entries(parsed as Record<string, unknown>);
|
|
386
385
|
const result: SecretMetadata[] = [];
|
|
387
386
|
for (const [, value] of entries) {
|
|
@@ -408,7 +407,8 @@ function parseSecretsList(stdout: string): SecretMetadata[] {
|
|
|
408
407
|
* Cloudflare secrets store does not expose per-secret type metadata,
|
|
409
408
|
* so we derive it from the key name suffix.
|
|
410
409
|
*/
|
|
411
|
-
|
|
410
|
+
/** Exported for unit tests — naming-convention secret type inference. */
|
|
411
|
+
export function inferSecretType(name: string): SecretMetadata["type"] {
|
|
412
412
|
const lower = name.toLowerCase();
|
|
413
413
|
if (lower.includes("key") || lower.includes("api")) return "api_key";
|
|
414
414
|
if (lower.includes("token")) return "token";
|
|
@@ -450,7 +450,8 @@ interface WranglerKeyRecord {
|
|
|
450
450
|
* This parser accepts both. Unknown rows are skipped; the parser never
|
|
451
451
|
* throws on malformed input — it returns an empty `keys` array instead.
|
|
452
452
|
*/
|
|
453
|
-
|
|
453
|
+
/** Exported for unit tests — pure KV list + manifest merge. */
|
|
454
|
+
export function parseKvList(
|
|
454
455
|
stdout: string,
|
|
455
456
|
manifest: Map<
|
|
456
457
|
string,
|
|
@@ -464,10 +465,10 @@ function parseKvList(
|
|
|
464
465
|
if (parsed === null) return [];
|
|
465
466
|
|
|
466
467
|
let records: unknown;
|
|
468
|
+
// `parsed` is non-null after the early return above.
|
|
467
469
|
if (Array.isArray(parsed)) {
|
|
468
470
|
records = parsed;
|
|
469
471
|
} else if (
|
|
470
|
-
parsed !== null &&
|
|
471
472
|
typeof parsed === "object" &&
|
|
472
473
|
"keys" in parsed &&
|
|
473
474
|
Array.isArray((parsed as { keys: unknown }).keys)
|
|
@@ -510,7 +511,8 @@ function parseKvList(
|
|
|
510
511
|
* return a `Map` keyed by key name. Returns an empty map on any failure
|
|
511
512
|
* — the view still functions, it just cannot flag secret keys.
|
|
512
513
|
*/
|
|
513
|
-
|
|
514
|
+
/** Exported for unit tests — pure KV manifest parser. */
|
|
515
|
+
export function parseKvManifest(
|
|
514
516
|
stdout: string
|
|
515
517
|
): Map<string, { type: "boolean" | "number" | "string"; secret: boolean }> {
|
|
516
518
|
const map = new Map<
|
|
@@ -524,8 +526,8 @@ function parseKvManifest(
|
|
|
524
526
|
if (parsed === null) return map;
|
|
525
527
|
|
|
526
528
|
// Manifest shape: { namespace: "CONFIG_KV", keys: [{ key, type, default, ... }] }
|
|
529
|
+
// `parsed` is non-null after the early return above.
|
|
527
530
|
const keys =
|
|
528
|
-
parsed !== null &&
|
|
529
531
|
typeof parsed === "object" &&
|
|
530
532
|
"keys" in parsed &&
|
|
531
533
|
Array.isArray((parsed as { keys: unknown }).keys)
|
|
@@ -551,8 +553,8 @@ function parseKvManifest(
|
|
|
551
553
|
// eslint-disable-next-line no-control-regex -- intentional: matches ESC (\x1b) sequences
|
|
552
554
|
const ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
553
555
|
|
|
554
|
-
/** Remove ANSI color escape codes from a string. */
|
|
555
|
-
function stripAnsi(input: string): string {
|
|
556
|
+
/** Remove ANSI color escape codes from a string. Exported for unit tests. */
|
|
557
|
+
export function stripAnsi(input: string): string {
|
|
556
558
|
return input.replace(ANSI_PATTERN, "");
|
|
557
559
|
}
|
|
558
560
|
|
|
@@ -579,7 +581,8 @@ interface WranglerQueueRecord {
|
|
|
579
581
|
* switches the format for set actions), so we detect the engaged/released
|
|
580
582
|
* state from the message text.
|
|
581
583
|
*/
|
|
582
|
-
|
|
584
|
+
/** Exported for unit tests — pure kill-switch status parser. */
|
|
585
|
+
export function parseKillSwitchStatus(
|
|
583
586
|
action: KillSwitchAction,
|
|
584
587
|
stdout: string
|
|
585
588
|
): KillSwitchStatus {
|
|
@@ -1521,7 +1524,8 @@ class CliBridgeImpl {
|
|
|
1521
1524
|
/**
|
|
1522
1525
|
* Parse `hoox pyne health --json` stdout into {@link PyneHealthResult}.
|
|
1523
1526
|
*/
|
|
1524
|
-
|
|
1527
|
+
/** Exported for unit tests — pure pyne health envelope parser. */
|
|
1528
|
+
export function parsePyneHealth(stdout: string): PyneHealthResult {
|
|
1525
1529
|
const timestamp = new Date().toISOString();
|
|
1526
1530
|
const cleaned = stdout.trim();
|
|
1527
1531
|
if (!cleaned) {
|
|
@@ -1567,7 +1571,8 @@ function parsePyneHealth(stdout: string): PyneHealthResult {
|
|
|
1567
1571
|
};
|
|
1568
1572
|
}
|
|
1569
1573
|
|
|
1570
|
-
|
|
1574
|
+
/** Exported for unit tests — pure agent health envelope parser. */
|
|
1575
|
+
export function parseAgentHealth(stdout: string): AgentHealthResult {
|
|
1571
1576
|
const timestamp = new Date().toISOString();
|
|
1572
1577
|
const cleaned = stdout.trim();
|
|
1573
1578
|
if (!cleaned) {
|
|
@@ -1624,7 +1629,8 @@ function parseAgentHealth(stdout: string): AgentHealthResult {
|
|
|
1624
1629
|
* exists so future manifest changes can light up the namespace column
|
|
1625
1630
|
* without touching the viewer.
|
|
1626
1631
|
*/
|
|
1627
|
-
|
|
1632
|
+
/** Exported for unit tests — reserved namespace-id extractor (currently always null). */
|
|
1633
|
+
export function extractNamespaceId(_manifestStdout: string): string | null {
|
|
1628
1634
|
// Reserved for future CLI versions — manifest envelope does not yet
|
|
1629
1635
|
// surface the namespace ID. Kept as a separate function to keep the
|
|
1630
1636
|
// test surface stable.
|
|
@@ -197,6 +197,33 @@ export function agentChatStream(
|
|
|
197
197
|
let completed = false;
|
|
198
198
|
|
|
199
199
|
const finished = (async () => {
|
|
200
|
+
// Match workers/SSE: Bearer + optional CF Access service-token headers
|
|
201
|
+
let authHeaders: Record<string, string> = {};
|
|
202
|
+
let resolvedBase = apiBase;
|
|
203
|
+
try {
|
|
204
|
+
const { resolveOperatorTransportProfile, buildOperatorAuthHeaders } =
|
|
205
|
+
await import("@hoox-sh/hoox-shared");
|
|
206
|
+
const profile = resolveOperatorTransportProfile(process.env as never, {
|
|
207
|
+
configApiUrl: apiBase,
|
|
208
|
+
configApiToken: apiToken,
|
|
209
|
+
});
|
|
210
|
+
authHeaders = buildOperatorAuthHeaders(profile);
|
|
211
|
+
// Prefer profile base when caller passed default localhost but env has remote
|
|
212
|
+
if (apiBase === "http://localhost:8787" && profile.apiBase) {
|
|
213
|
+
resolvedBase = profile.apiBase;
|
|
214
|
+
} else {
|
|
215
|
+
resolvedBase = apiBase.replace(/\/$/, "") || profile.apiBase;
|
|
216
|
+
}
|
|
217
|
+
// Ensure Bearer from explicit apiToken if profile missed it
|
|
218
|
+
if (apiToken && !authHeaders.Authorization) {
|
|
219
|
+
authHeaders.Authorization = `Bearer ${apiToken}`;
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
if (apiToken) {
|
|
223
|
+
authHeaders = { Authorization: `Bearer ${apiToken}` };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
200
227
|
let attempt = 0;
|
|
201
228
|
|
|
202
229
|
while (
|
|
@@ -204,7 +231,7 @@ export function agentChatStream(
|
|
|
204
231
|
attempt < SSE_MAX_RECONNECT_ATTEMPTS
|
|
205
232
|
) {
|
|
206
233
|
try {
|
|
207
|
-
const url = `${
|
|
234
|
+
const url = `${resolvedBase.replace(/\/$/, "")}/api/agent/chat`;
|
|
208
235
|
const body = JSON.stringify({
|
|
209
236
|
messages: params.messages.map((m) => ({
|
|
210
237
|
role: m.role,
|
|
@@ -224,7 +251,7 @@ export function agentChatStream(
|
|
|
224
251
|
headers: {
|
|
225
252
|
"Content-Type": "application/json",
|
|
226
253
|
Accept: "text/event-stream",
|
|
227
|
-
...
|
|
254
|
+
...authHeaders,
|
|
228
255
|
},
|
|
229
256
|
body,
|
|
230
257
|
signal: abortController.signal,
|