@docyrus/docyrus 0.0.4 → 0.0.7

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/tui.mjs ADDED
@@ -0,0 +1,1503 @@
1
+ // src/tui/opentuiMain.tsx
2
+ import { createCliRenderer } from "@opentui/core";
3
+ import { createRoot } from "@opentui/react";
4
+
5
+ // src/tui/opentui/DocyrusOpenTuiApp.tsx
6
+ import { useKeyboard as useKeyboard2, useRenderer } from "@opentui/react";
7
+ import { useCallback as useCallback2, useEffect, useMemo, useRef, useState as useState2 } from "react";
8
+
9
+ // src/services/tuiProcessExecutor.ts
10
+ import { spawnSync } from "node:child_process";
11
+
12
+ // src/errors.ts
13
+ var ApplicationError = class extends Error {
14
+ constructor(message, data = {}, httpStatusCode = 500) {
15
+ super(message);
16
+ this.data = data;
17
+ this.httpStatusCode = httpStatusCode;
18
+ }
19
+ };
20
+ var UserInputError = class extends ApplicationError {
21
+ constructor(message, data = {}) {
22
+ super(message, data, 400);
23
+ }
24
+ };
25
+
26
+ // src/services/tuiProcessExecutor.ts
27
+ function normalizeCommandArg(arg) {
28
+ return arg.trim();
29
+ }
30
+ function sanitizeTuiCommandArgs(args) {
31
+ const normalized = args.map(normalizeCommandArg).filter((value) => value.length > 0);
32
+ const withoutBinary = normalized[0] === "docyrus" ? normalized.slice(1) : normalized;
33
+ const withoutJson = withoutBinary.filter((value) => value !== "--json");
34
+ const firstArg = withoutJson[0];
35
+ if (firstArg === "tui" || firstArg === "interactive") {
36
+ throw new UserInputError("Already in TUI session.");
37
+ }
38
+ return withoutJson;
39
+ }
40
+ function parseJsonOutput(rawOutput) {
41
+ const trimmedOutput = rawOutput.trim();
42
+ if (!trimmedOutput) {
43
+ throw new UserInputError("Command produced no output.");
44
+ }
45
+ const lines = trimmedOutput.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
46
+ const lastLine = lines.at(-1);
47
+ if (lastLine) {
48
+ try {
49
+ return {
50
+ data: JSON.parse(lastLine),
51
+ messages: lines.slice(0, -1)
52
+ };
53
+ } catch {
54
+ }
55
+ }
56
+ try {
57
+ return {
58
+ data: JSON.parse(trimmedOutput),
59
+ messages: []
60
+ };
61
+ } catch (error) {
62
+ throw new UserInputError("Invalid command output format. Expected JSON output.", {
63
+ cause: error,
64
+ rawOutput: trimmedOutput
65
+ });
66
+ }
67
+ }
68
+ function toErrorMessage(error) {
69
+ if (error instanceof Error && error.message.trim().length > 0) {
70
+ return error.message;
71
+ }
72
+ return "Failed to execute command.";
73
+ }
74
+ function createTuiProcessExecutor(options) {
75
+ const spawnCommand = options.spawnSyncFn ?? spawnSync;
76
+ const now = options.now ?? Date.now;
77
+ const runCliCommand = async (args) => {
78
+ const startedAt = now();
79
+ let commandArgs = [];
80
+ let commandLabel = "";
81
+ let rawOutput = "";
82
+ let messageLines = [];
83
+ try {
84
+ commandArgs = sanitizeTuiCommandArgs(args);
85
+ commandLabel = commandArgs.join(" ").trim();
86
+ const spawnResult = spawnCommand(
87
+ options.executionConfig.executable,
88
+ [
89
+ options.executionConfig.scriptPath,
90
+ ...commandArgs,
91
+ "--json"
92
+ ],
93
+ {
94
+ encoding: "utf8",
95
+ cwd: options.executionConfig.cwd,
96
+ env: {
97
+ ...process.env,
98
+ ...options.executionConfig.env || {}
99
+ }
100
+ }
101
+ );
102
+ const stdout = spawnResult.stdout?.toString() || "";
103
+ const stderr = spawnResult.stderr?.toString() || "";
104
+ rawOutput = stdout.trim();
105
+ if (spawnResult.error) {
106
+ throw spawnResult.error;
107
+ }
108
+ if (spawnResult.status !== 0) {
109
+ const details = [rawOutput, stderr.trim()].filter((line) => line.length > 0).join("\n");
110
+ return {
111
+ ok: false,
112
+ command: commandLabel,
113
+ rawOutput: details,
114
+ error: {
115
+ message: stderr.trim() || `Command exited with code ${spawnResult.status ?? "unknown"}.`,
116
+ details: details || void 0
117
+ },
118
+ durationMs: now() - startedAt
119
+ };
120
+ }
121
+ const parsed = parseJsonOutput(stdout);
122
+ messageLines = parsed.messages;
123
+ return {
124
+ ok: true,
125
+ command: commandLabel,
126
+ rawOutput,
127
+ data: parsed.data,
128
+ durationMs: now() - startedAt,
129
+ messages: messageLines.length > 0 ? messageLines : void 0
130
+ };
131
+ } catch (error) {
132
+ return {
133
+ ok: false,
134
+ command: commandLabel,
135
+ rawOutput,
136
+ error: {
137
+ message: toErrorMessage(error),
138
+ details: rawOutput || void 0
139
+ },
140
+ durationMs: now() - startedAt,
141
+ messages: messageLines.length > 0 ? messageLines : void 0
142
+ };
143
+ }
144
+ };
145
+ return {
146
+ runCliCommand
147
+ };
148
+ }
149
+
150
+ // src/tui/opentui/commandInput.ts
151
+ import { useKeyboard } from "@opentui/react";
152
+ import { useCallback, useState } from "react";
153
+ function parseCommandLine(command) {
154
+ const args = [];
155
+ let current = "";
156
+ let quote = null;
157
+ let escaped = false;
158
+ for (const char of command) {
159
+ if (escaped) {
160
+ current += char;
161
+ escaped = false;
162
+ continue;
163
+ }
164
+ if (char === "\\") {
165
+ escaped = true;
166
+ continue;
167
+ }
168
+ if (quote) {
169
+ if (char === quote) {
170
+ quote = null;
171
+ } else {
172
+ current += char;
173
+ }
174
+ continue;
175
+ }
176
+ if (char === '"' || char === "'") {
177
+ quote = char;
178
+ continue;
179
+ }
180
+ if (/\s/.test(char)) {
181
+ if (current.length > 0) {
182
+ args.push(current);
183
+ current = "";
184
+ }
185
+ continue;
186
+ }
187
+ current += char;
188
+ }
189
+ if (quote) {
190
+ throw new Error("Invalid command syntax: unmatched quote.");
191
+ }
192
+ if (escaped) {
193
+ current += "\\";
194
+ }
195
+ if (current.length > 0) {
196
+ args.push(current);
197
+ }
198
+ return args;
199
+ }
200
+ function preventDefault(event) {
201
+ if (typeof event.preventDefault === "function") {
202
+ event.preventDefault();
203
+ }
204
+ }
205
+ function useCommandInput(options) {
206
+ const [history, setHistory] = useState([]);
207
+ const [historyCursor, setHistoryCursor] = useState(null);
208
+ const [draftInput, setDraftInput] = useState("");
209
+ const submitInput = useCallback((value) => {
210
+ if (options.busy) {
211
+ return;
212
+ }
213
+ const trimmed = (value ?? options.input).trim();
214
+ if (trimmed.length === 0) {
215
+ return;
216
+ }
217
+ setHistory((previous) => {
218
+ if (previous[0] === trimmed) {
219
+ return previous;
220
+ }
221
+ return [trimmed, ...previous];
222
+ });
223
+ setHistoryCursor(null);
224
+ setDraftInput("");
225
+ options.setInput("");
226
+ options.onSubmit(trimmed);
227
+ }, [options]);
228
+ const recallHistory = useCallback((direction) => {
229
+ if (history.length === 0) {
230
+ return;
231
+ }
232
+ if (direction === "up") {
233
+ if (historyCursor === null) {
234
+ setDraftInput(options.input);
235
+ setHistoryCursor(0);
236
+ options.setInput(history[0] || "");
237
+ return;
238
+ }
239
+ const nextCursor2 = Math.min(historyCursor + 1, history.length - 1);
240
+ setHistoryCursor(nextCursor2);
241
+ options.setInput(history[nextCursor2] || "");
242
+ return;
243
+ }
244
+ if (historyCursor === null) {
245
+ return;
246
+ }
247
+ const nextCursor = historyCursor - 1;
248
+ if (nextCursor < 0) {
249
+ setHistoryCursor(null);
250
+ options.setInput(draftInput);
251
+ return;
252
+ }
253
+ setHistoryCursor(nextCursor);
254
+ options.setInput(history[nextCursor] || "");
255
+ }, [draftInput, history, historyCursor, options]);
256
+ useKeyboard((keyEvent) => {
257
+ if (options.isModalOpen) {
258
+ return;
259
+ }
260
+ const keyName = keyEvent.name;
261
+ const hotkeysEnabled = options.hotkeysEnabled !== false;
262
+ if (keyEvent.ctrl && keyName === "l") {
263
+ preventDefault(keyEvent);
264
+ options.onClear();
265
+ return;
266
+ }
267
+ if (hotkeysEnabled && keyName === "tab") {
268
+ preventDefault(keyEvent);
269
+ if (keyEvent.shift) {
270
+ options.onPreviousSection();
271
+ return;
272
+ }
273
+ options.onNextSection();
274
+ return;
275
+ }
276
+ if (hotkeysEnabled && keyName === "[") {
277
+ preventDefault(keyEvent);
278
+ options.onPreviousSection();
279
+ return;
280
+ }
281
+ if (hotkeysEnabled && keyName === "]") {
282
+ preventDefault(keyEvent);
283
+ options.onNextSection();
284
+ return;
285
+ }
286
+ if (options.isInputFocused && keyName === "up") {
287
+ preventDefault(keyEvent);
288
+ recallHistory("up");
289
+ return;
290
+ }
291
+ if (options.isInputFocused && keyName === "down") {
292
+ preventDefault(keyEvent);
293
+ recallHistory("down");
294
+ return;
295
+ }
296
+ if (options.busy) {
297
+ return;
298
+ }
299
+ if (hotkeysEnabled && options.input.length === 0 && /^[1-6]$/.test(keyName)) {
300
+ preventDefault(keyEvent);
301
+ options.onShortcut(Number(keyName));
302
+ }
303
+ });
304
+ return {
305
+ submitInput
306
+ };
307
+ }
308
+
309
+ // src/tui/opentui/dataSourceCatalog.ts
310
+ function isRecord(value) {
311
+ return typeof value === "object" && value !== null && !Array.isArray(value);
312
+ }
313
+ function toNonEmptyString(value) {
314
+ if (typeof value !== "string") {
315
+ return void 0;
316
+ }
317
+ const trimmed = value.trim();
318
+ return trimmed.length > 0 ? trimmed : void 0;
319
+ }
320
+ function toStringValue(value, fallback) {
321
+ return toNonEmptyString(value) || fallback;
322
+ }
323
+ function getArrayCandidates(payload) {
324
+ if (Array.isArray(payload)) {
325
+ return payload;
326
+ }
327
+ if (!isRecord(payload)) {
328
+ return [];
329
+ }
330
+ const directData = payload.data;
331
+ if (Array.isArray(directData)) {
332
+ return directData;
333
+ }
334
+ if (isRecord(directData) && Array.isArray(directData.data)) {
335
+ return directData.data;
336
+ }
337
+ if (Array.isArray(payload.items)) {
338
+ return payload.items;
339
+ }
340
+ if (Array.isArray(payload.dataSources)) {
341
+ return payload.dataSources;
342
+ }
343
+ return [];
344
+ }
345
+ function compareByName(a, b) {
346
+ return a.localeCompare(b, void 0, { sensitivity: "base" });
347
+ }
348
+ function getDataSourceDisplayName(dataSource) {
349
+ return dataSource.title || dataSource.name || dataSource.slug;
350
+ }
351
+ function extractAppsFromPayload(payload) {
352
+ const result = [];
353
+ const seen = /* @__PURE__ */ new Set();
354
+ for (const candidate of getArrayCandidates(payload)) {
355
+ if (!isRecord(candidate)) {
356
+ continue;
357
+ }
358
+ const id = toNonEmptyString(candidate.id);
359
+ const slug = toNonEmptyString(candidate.slug);
360
+ if (!id || !slug) {
361
+ continue;
362
+ }
363
+ const name = toStringValue(candidate.name, slug);
364
+ const dedupeKey = `${id}::${slug}`;
365
+ if (seen.has(dedupeKey)) {
366
+ continue;
367
+ }
368
+ seen.add(dedupeKey);
369
+ result.push({ id, slug, name });
370
+ }
371
+ result.sort((a, b) => compareByName(a.name, b.name));
372
+ return result;
373
+ }
374
+ function extractDataSourcesFromPayload(payload) {
375
+ const result = [];
376
+ const seen = /* @__PURE__ */ new Set();
377
+ for (const candidate of getArrayCandidates(payload)) {
378
+ if (!isRecord(candidate)) {
379
+ continue;
380
+ }
381
+ const id = toNonEmptyString(candidate.id);
382
+ const slug = toNonEmptyString(candidate.slug);
383
+ if (!id || !slug) {
384
+ continue;
385
+ }
386
+ const title = toNonEmptyString(candidate.title);
387
+ const name = toStringValue(candidate.name, slug);
388
+ const appSlug = toNonEmptyString(candidate.app_slug);
389
+ const tenantAppId = toNonEmptyString(candidate.tenant_app_id);
390
+ const dedupeKey = `${id}::${slug}`;
391
+ if (seen.has(dedupeKey)) {
392
+ continue;
393
+ }
394
+ seen.add(dedupeKey);
395
+ result.push({
396
+ id,
397
+ name,
398
+ title,
399
+ slug,
400
+ app_slug: appSlug,
401
+ tenant_app_id: tenantAppId
402
+ });
403
+ }
404
+ result.sort((a, b) => compareByName(getDataSourceDisplayName(a), getDataSourceDisplayName(b)));
405
+ return result;
406
+ }
407
+ function groupDataSourcesByApp(params) {
408
+ const { apps, dataSources } = params;
409
+ const appById = /* @__PURE__ */ new Map();
410
+ const appBySlug = /* @__PURE__ */ new Map();
411
+ const groups = /* @__PURE__ */ new Map();
412
+ for (const app of apps) {
413
+ appById.set(app.id, app);
414
+ appBySlug.set(app.slug, app);
415
+ }
416
+ for (const dataSource of dataSources) {
417
+ const byId = dataSource.tenant_app_id ? appById.get(dataSource.tenant_app_id) : void 0;
418
+ const bySlug = dataSource.app_slug ? appBySlug.get(dataSource.app_slug) : void 0;
419
+ const matchedApp = byId || bySlug;
420
+ const appKey = matchedApp ? matchedApp.id : dataSource.app_slug ? `slug:${dataSource.app_slug}` : "unknown";
421
+ const group = groups.get(appKey) || {
422
+ appKey,
423
+ appId: matchedApp?.id || dataSource.tenant_app_id,
424
+ appName: matchedApp?.name || dataSource.app_slug || "Unknown App",
425
+ appSlug: matchedApp?.slug || dataSource.app_slug,
426
+ dataSources: []
427
+ };
428
+ group.dataSources.push(dataSource);
429
+ groups.set(appKey, group);
430
+ }
431
+ const list = Array.from(groups.values());
432
+ for (const group of list) {
433
+ group.dataSources.sort((a, b) => compareByName(getDataSourceDisplayName(a), getDataSourceDisplayName(b)));
434
+ }
435
+ list.sort((a, b) => compareByName(a.appName, b.appName));
436
+ return list;
437
+ }
438
+ function filterDataSources(groups, query) {
439
+ const normalizedQuery = query.trim().toLowerCase();
440
+ if (normalizedQuery.length === 0) {
441
+ return groups;
442
+ }
443
+ return groups.map((group) => {
444
+ const filteredDataSources = group.dataSources.filter((dataSource) => {
445
+ const haystack = [
446
+ dataSource.title,
447
+ dataSource.name,
448
+ dataSource.slug
449
+ ].filter((value) => Boolean(value)).join(" ").toLowerCase();
450
+ return haystack.includes(normalizedQuery);
451
+ });
452
+ return {
453
+ ...group,
454
+ dataSources: filteredDataSources
455
+ };
456
+ }).filter((group) => group.dataSources.length > 0);
457
+ }
458
+ function flattenTreeRows(params) {
459
+ const rows = [];
460
+ const alwaysExpanded = params.query.trim().length > 0;
461
+ for (const group of params.groups) {
462
+ const expanded = alwaysExpanded || params.expandedAppKeys.has(group.appKey);
463
+ rows.push({
464
+ id: `app:${group.appKey}`,
465
+ kind: "app",
466
+ appKey: group.appKey,
467
+ appName: group.appName,
468
+ appSlug: group.appSlug,
469
+ expanded
470
+ });
471
+ if (!expanded) {
472
+ continue;
473
+ }
474
+ for (const dataSource of group.dataSources) {
475
+ const dataSourceName = getDataSourceDisplayName(dataSource);
476
+ rows.push({
477
+ id: `ds:${group.appKey}:${dataSource.id}`,
478
+ kind: "datasource",
479
+ appKey: group.appKey,
480
+ appName: group.appName,
481
+ appSlug: group.appSlug || dataSource.app_slug,
482
+ dataSourceId: dataSource.id,
483
+ dataSourceName,
484
+ dataSourceSlug: dataSource.slug
485
+ });
486
+ }
487
+ }
488
+ return rows;
489
+ }
490
+
491
+ // src/tui/opentui/renderResult.tsx
492
+ import { RGBA, SyntaxStyle } from "@opentui/core";
493
+
494
+ // src/tui/opentui/resultDocument.ts
495
+ function toErrorPayload(entry) {
496
+ return {
497
+ message: entry.result.error?.message || "Command failed.",
498
+ details: entry.result.error?.details
499
+ };
500
+ }
501
+ function buildPayload(entry) {
502
+ if (entry.result.ok) {
503
+ return entry.result.data ?? null;
504
+ }
505
+ return toErrorPayload(entry);
506
+ }
507
+ function buildResultDocument(entry) {
508
+ return {
509
+ command: entry.command || "(root)",
510
+ ok: entry.result.ok,
511
+ durationMs: entry.result.durationMs,
512
+ timestamp: new Date(entry.createdAt).toISOString(),
513
+ payload: buildPayload(entry),
514
+ messages: entry.result.messages,
515
+ rawOutput: entry.result.rawOutput || void 0
516
+ };
517
+ }
518
+ function serializeResultDocument(document) {
519
+ try {
520
+ const result = JSON.stringify(document, null, 2);
521
+ if (!result) {
522
+ return "null";
523
+ }
524
+ return result;
525
+ } catch (error) {
526
+ const message = error instanceof Error ? error.message : String(error);
527
+ return JSON.stringify({
528
+ command: document.command,
529
+ ok: false,
530
+ durationMs: document.durationMs,
531
+ timestamp: document.timestamp,
532
+ payload: {
533
+ message: "Serialization failed",
534
+ details: message
535
+ }
536
+ }, null, 2);
537
+ }
538
+ }
539
+
540
+ // src/tui/opentui/renderResult.tsx
541
+ import { jsx, jsxs } from "@opentui/react/jsx-runtime";
542
+ var jsonSyntaxStyle = null;
543
+ function getJsonSyntaxStyle() {
544
+ if (jsonSyntaxStyle) {
545
+ return jsonSyntaxStyle;
546
+ }
547
+ jsonSyntaxStyle = SyntaxStyle.fromStyles({
548
+ default: {
549
+ fg: RGBA.fromHex("#d4d4d4")
550
+ },
551
+ property: {
552
+ fg: RGBA.fromHex("#9cdcfe")
553
+ },
554
+ string: {
555
+ fg: RGBA.fromHex("#ce9178")
556
+ },
557
+ number: {
558
+ fg: RGBA.fromHex("#b5cea8")
559
+ },
560
+ keyword: {
561
+ fg: RGBA.fromHex("#569cd6")
562
+ },
563
+ constant: {
564
+ fg: RGBA.fromHex("#4fc1ff")
565
+ },
566
+ punctuation: {
567
+ fg: RGBA.fromHex("#c586c0")
568
+ },
569
+ operator: {
570
+ fg: RGBA.fromHex("#d4d4d4")
571
+ }
572
+ });
573
+ return jsonSyntaxStyle;
574
+ }
575
+ function renderResult(params) {
576
+ const {
577
+ entry,
578
+ isFocused
579
+ } = params;
580
+ if (!entry) {
581
+ return /* @__PURE__ */ jsx("text", { fg: "yellow", children: "No command executed yet." });
582
+ }
583
+ const document = buildResultDocument(entry);
584
+ const json = serializeResultDocument(document);
585
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "column", width: "100%", height: "100%", children: [
586
+ /* @__PURE__ */ jsx("box", { paddingBottom: 1, children: /* @__PURE__ */ jsxs("text", { fg: document.ok ? "green" : "red", children: [
587
+ document.ok ? "OK" : "ERR",
588
+ " ",
589
+ document.command,
590
+ " [",
591
+ document.durationMs,
592
+ "ms]"
593
+ ] }) }),
594
+ /* @__PURE__ */ jsx("box", { flexGrow: 1, width: "100%", height: "100%", children: /* @__PURE__ */ jsx("scrollbox", { focused: isFocused, style: { height: "100%" }, children: /* @__PURE__ */ jsx(
595
+ "code",
596
+ {
597
+ width: "100%",
598
+ content: json,
599
+ filetype: "json",
600
+ syntaxStyle: getJsonSyntaxStyle(),
601
+ streaming: false,
602
+ conceal: false
603
+ }
604
+ ) }) })
605
+ ] });
606
+ }
607
+
608
+ // src/tui/opentui/DocyrusOpenTuiApp.tsx
609
+ import { jsx as jsx2, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
610
+ var SHORTCUTS = [
611
+ {
612
+ id: 1,
613
+ run: "env list",
614
+ label: "env list"
615
+ },
616
+ {
617
+ id: 2,
618
+ run: "auth who",
619
+ label: "auth who"
620
+ },
621
+ {
622
+ id: 3,
623
+ run: "apps list",
624
+ label: "apps list"
625
+ },
626
+ {
627
+ id: 4,
628
+ run: "discover namespaces",
629
+ label: "discover namespaces"
630
+ },
631
+ {
632
+ id: 5,
633
+ template: "studio list-data-sources --appSlug ",
634
+ label: "insert: studio list-data-sources --appSlug "
635
+ },
636
+ {
637
+ id: 6,
638
+ template: "ds list <appSlug> <dataSourceSlug>",
639
+ label: "insert: ds list <appSlug> <dataSourceSlug>"
640
+ }
641
+ ];
642
+ var SECTION_ORDER = ["shortcuts", "datasources", "history", "messages", "help"];
643
+ var FOCUS_ORDER = ["input", "left", "result"];
644
+ var ESC_ARM_TIMEOUT_MS = 1400;
645
+ function isRecord2(value) {
646
+ return typeof value === "object" && value !== null && !Array.isArray(value);
647
+ }
648
+ function extractMetadata(payload) {
649
+ if (!isRecord2(payload)) {
650
+ return {};
651
+ }
652
+ const environmentValue = payload.environment;
653
+ const contextValue = payload.context;
654
+ const environment = isRecord2(environmentValue) && typeof environmentValue.id === "string" && typeof environmentValue.name === "string" ? { id: environmentValue.id, name: environmentValue.name } : void 0;
655
+ if (contextValue === null) {
656
+ return {
657
+ environment,
658
+ context: null
659
+ };
660
+ }
661
+ const context = isRecord2(contextValue) && typeof contextValue.email === "string" && typeof contextValue.tenantDisplay === "string" ? {
662
+ email: contextValue.email,
663
+ tenantDisplay: contextValue.tenantDisplay
664
+ } : void 0;
665
+ return {
666
+ environment,
667
+ context
668
+ };
669
+ }
670
+ function toFailedResult(error, command) {
671
+ const message = error instanceof Error ? error.message : "Invalid command syntax.";
672
+ return {
673
+ ok: false,
674
+ command,
675
+ rawOutput: "",
676
+ error: {
677
+ message
678
+ },
679
+ durationMs: 0
680
+ };
681
+ }
682
+ function formatHistoryLabel(entry) {
683
+ const status = entry.result.ok ? "OK" : "ERR";
684
+ const command = entry.command || "(root)";
685
+ return `${status} ${command} [${entry.result.durationMs}ms]`;
686
+ }
687
+ function preventDefault2(keyEvent) {
688
+ if (typeof keyEvent.preventDefault === "function") {
689
+ keyEvent.preventDefault();
690
+ }
691
+ }
692
+ function isEscapeKey(keyName) {
693
+ return keyName === "escape" || keyName === "esc";
694
+ }
695
+ function isEnterKey(keyName) {
696
+ return keyName === "enter" || keyName === "return";
697
+ }
698
+ function isSlashKey(keyName) {
699
+ return keyName === "/" || keyName === "slash";
700
+ }
701
+ function toCatalogErrorMessage(command, result) {
702
+ const details = result.error?.details || result.error?.message || "Unknown error.";
703
+ if (details.includes("Architect.Read.All") || details.includes("Architect.ReadWrite.All")) {
704
+ return `${command} failed: missing required scope Architect.Read.All or Architect.ReadWrite.All.`;
705
+ }
706
+ const message = result.error?.message || "Command failed.";
707
+ return `${command} failed: ${message}`;
708
+ }
709
+ function DocyrusOpenTuiApp(props) {
710
+ const renderer = useRenderer();
711
+ const [input, setInput] = useState2("");
712
+ const [entries, setEntries] = useState2([]);
713
+ const [activeEntryId, setActiveEntryId] = useState2(null);
714
+ const [systemMessages, setSystemMessages] = useState2([]);
715
+ const [activeSection, setActiveSection] = useState2("shortcuts");
716
+ const [focusedPanel, setFocusedPanel] = useState2("left");
717
+ const [environment, setEnvironment] = useState2();
718
+ const [context, setContext] = useState2(void 0);
719
+ const [isRunning, setIsRunning] = useState2(false);
720
+ const [spinnerFrame, setSpinnerFrame] = useState2(0);
721
+ const [selectedShortcutIndex, setSelectedShortcutIndex] = useState2(0);
722
+ const [selectedHistoryIndex, setSelectedHistoryIndex] = useState2(0);
723
+ const [isExitConfirmOpen, setIsExitConfirmOpen] = useState2(false);
724
+ const [isEscArmed, setIsEscArmed] = useState2(false);
725
+ const [dataSourceSearch, setDataSourceSearch] = useState2("");
726
+ const [dataSourcePanelFocus, setDataSourcePanelFocus] = useState2("tree");
727
+ const [selectedDataSourceRowIndex, setSelectedDataSourceRowIndex] = useState2(0);
728
+ const [expandedAppKeys, setExpandedAppKeys] = useState2([]);
729
+ const [appsCatalog, setAppsCatalog] = useState2([]);
730
+ const [dataSourcesCatalog, setDataSourcesCatalog] = useState2([]);
731
+ const [isCatalogLoading, setIsCatalogLoading] = useState2(false);
732
+ const [catalogError, setCatalogError] = useState2(null);
733
+ const [catalogLoadedAt, setCatalogLoadedAt] = useState2(null);
734
+ const catalogScopeRef = useRef("");
735
+ const executor = useMemo(() => {
736
+ return createTuiProcessExecutor({
737
+ executionConfig: props.executionConfig
738
+ });
739
+ }, [props.executionConfig]);
740
+ const applyMetadataFromPayload = useCallback2((payload) => {
741
+ const metadata = extractMetadata(payload);
742
+ if (metadata.environment) {
743
+ setEnvironment(metadata.environment);
744
+ }
745
+ if (metadata.context !== void 0) {
746
+ setContext(metadata.context);
747
+ }
748
+ }, []);
749
+ const appendSystemMessages = useCallback2((messages) => {
750
+ if (messages.length === 0) {
751
+ return;
752
+ }
753
+ setSystemMessages((previous) => [...previous, ...messages]);
754
+ }, []);
755
+ const applyCommandResult = useCallback2((result, command) => {
756
+ const entry = {
757
+ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
758
+ command,
759
+ result,
760
+ createdAt: Date.now()
761
+ };
762
+ setEntries((previous) => [entry, ...previous]);
763
+ setActiveEntryId(entry.id);
764
+ setSelectedHistoryIndex(0);
765
+ if (result.messages && result.messages.length > 0) {
766
+ appendSystemMessages(result.messages);
767
+ }
768
+ if (result.ok && result.data !== void 0) {
769
+ applyMetadataFromPayload(result.data);
770
+ }
771
+ }, [appendSystemMessages, applyMetadataFromPayload]);
772
+ const executeArgs = useCallback2(async (args, commandText) => {
773
+ setIsRunning(true);
774
+ try {
775
+ const result = await executor.runCliCommand(args);
776
+ applyCommandResult(result, commandText);
777
+ } finally {
778
+ setIsRunning(false);
779
+ }
780
+ }, [applyCommandResult, executor]);
781
+ const runInternalArgs = useCallback2(async (args) => {
782
+ const result = await executor.runCliCommand(args);
783
+ if (result.ok && result.data !== void 0) {
784
+ applyMetadataFromPayload(result.data);
785
+ }
786
+ return result;
787
+ }, [applyMetadataFromPayload, executor]);
788
+ const executeLine = useCallback2((line) => {
789
+ let args;
790
+ try {
791
+ args = parseCommandLine(line);
792
+ } catch (error) {
793
+ applyCommandResult(toFailedResult(error, line.trim()), line.trim());
794
+ return;
795
+ }
796
+ if (args.length === 0) {
797
+ return;
798
+ }
799
+ void executeArgs(args, line.trim());
800
+ }, [applyCommandResult, executeArgs]);
801
+ const runHistoryEntry = useCallback2(async (entry) => {
802
+ if (entry.command === "(root)") {
803
+ await executeArgs([], "(root)");
804
+ return;
805
+ }
806
+ try {
807
+ const args = parseCommandLine(entry.command);
808
+ await executeArgs(args, entry.command);
809
+ } catch (error) {
810
+ applyCommandResult(toFailedResult(error, entry.command), entry.command);
811
+ }
812
+ }, [applyCommandResult, executeArgs]);
813
+ const applyShortcut = useCallback2((shortcutId) => {
814
+ const action = SHORTCUTS.find((shortcut) => shortcut.id === shortcutId);
815
+ if (!action || isRunning || isCatalogLoading) {
816
+ return;
817
+ }
818
+ if (action.template) {
819
+ setInput(action.template);
820
+ setFocusedPanel("input");
821
+ return;
822
+ }
823
+ if (action.run) {
824
+ void executeArgs(parseCommandLine(action.run), action.run);
825
+ }
826
+ }, [executeArgs, isCatalogLoading, isRunning]);
827
+ const groupedDataSourceItems = useMemo(() => {
828
+ return groupDataSourcesByApp({
829
+ apps: appsCatalog,
830
+ dataSources: dataSourcesCatalog
831
+ });
832
+ }, [appsCatalog, dataSourcesCatalog]);
833
+ const filteredDataSourceGroups = useMemo(() => {
834
+ return filterDataSources(groupedDataSourceItems, dataSourceSearch);
835
+ }, [dataSourceSearch, groupedDataSourceItems]);
836
+ const dataSourceCountByAppKey = useMemo(() => {
837
+ const result = /* @__PURE__ */ new Map();
838
+ for (const group of filteredDataSourceGroups) {
839
+ result.set(group.appKey, group.dataSources.length);
840
+ }
841
+ return result;
842
+ }, [filteredDataSourceGroups]);
843
+ const expandedAppKeySet = useMemo(() => {
844
+ return new Set(expandedAppKeys);
845
+ }, [expandedAppKeys]);
846
+ const dataSourceRows = useMemo(() => {
847
+ return flattenTreeRows({
848
+ groups: filteredDataSourceGroups,
849
+ expandedAppKeys: expandedAppKeySet,
850
+ query: dataSourceSearch
851
+ });
852
+ }, [dataSourceSearch, expandedAppKeySet, filteredDataSourceGroups]);
853
+ const selectedDataSourceRow = useMemo(() => {
854
+ if (dataSourceRows.length === 0) {
855
+ return null;
856
+ }
857
+ return dataSourceRows[selectedDataSourceRowIndex] || dataSourceRows[0] || null;
858
+ }, [dataSourceRows, selectedDataSourceRowIndex]);
859
+ const toggleAppExpansion = useCallback2((appKey) => {
860
+ setExpandedAppKeys((previous) => {
861
+ if (previous.includes(appKey)) {
862
+ return previous.filter((key) => key !== appKey);
863
+ }
864
+ return [...previous, appKey];
865
+ });
866
+ }, []);
867
+ const loadDataSourceCatalog = useCallback2(async () => {
868
+ if (isCatalogLoading) {
869
+ return;
870
+ }
871
+ setIsCatalogLoading(true);
872
+ setCatalogError(null);
873
+ try {
874
+ const nextMessages = [];
875
+ const appsResult = await runInternalArgs(["apps", "list"]);
876
+ const dataSourcesResult = await runInternalArgs(["curl", "/dev/data-sources"]);
877
+ const nextApps = appsResult.ok && appsResult.data !== void 0 ? extractAppsFromPayload(appsResult.data) : [];
878
+ const nextDataSources = dataSourcesResult.ok && dataSourcesResult.data !== void 0 ? extractDataSourcesFromPayload(dataSourcesResult.data) : [];
879
+ if (!appsResult.ok) {
880
+ nextMessages.push(toCatalogErrorMessage("apps list", appsResult));
881
+ }
882
+ if (!dataSourcesResult.ok) {
883
+ nextMessages.push(toCatalogErrorMessage("curl /dev/data-sources", dataSourcesResult));
884
+ }
885
+ setAppsCatalog(nextApps);
886
+ setDataSourcesCatalog(nextDataSources);
887
+ setCatalogLoadedAt(Date.now());
888
+ const nextGroups = groupDataSourcesByApp({
889
+ apps: nextApps,
890
+ dataSources: nextDataSources
891
+ });
892
+ setExpandedAppKeys((previous) => {
893
+ if (previous.length > 0) {
894
+ const allowed = new Set(nextGroups.map((group) => group.appKey));
895
+ return previous.filter((key) => allowed.has(key));
896
+ }
897
+ return nextGroups.map((group) => group.appKey);
898
+ });
899
+ if (nextMessages.length > 0) {
900
+ appendSystemMessages(nextMessages);
901
+ setCatalogError(nextMessages[0] || "Failed to load catalog.");
902
+ } else {
903
+ setCatalogError(null);
904
+ }
905
+ } finally {
906
+ setIsCatalogLoading(false);
907
+ }
908
+ }, [appendSystemMessages, isCatalogLoading, runInternalArgs]);
909
+ const runSelectedDataSourceRow = useCallback2(async (row) => {
910
+ if (!row || isRunning || isCatalogLoading) {
911
+ return;
912
+ }
913
+ if (row.kind === "app") {
914
+ toggleAppExpansion(row.appKey);
915
+ return;
916
+ }
917
+ if (!row.appSlug || !row.dataSourceSlug) {
918
+ appendSystemMessages([
919
+ "Cannot execute ds get because selected row is missing appSlug or dataSourceSlug."
920
+ ]);
921
+ return;
922
+ }
923
+ const command = `ds get ${row.appSlug} ${row.dataSourceSlug}`;
924
+ setFocusedPanel("result");
925
+ await executeArgs(["ds", "get", row.appSlug, row.dataSourceSlug], command);
926
+ }, [appendSystemMessages, executeArgs, isCatalogLoading, isRunning, toggleAppExpansion]);
927
+ const setSectionByIndex = useCallback2((index) => {
928
+ const safeIndex = index < 0 ? SECTION_ORDER.length - 1 : index >= SECTION_ORDER.length ? 0 : index;
929
+ const section = SECTION_ORDER[safeIndex];
930
+ if (section) {
931
+ setActiveSection(section);
932
+ }
933
+ }, []);
934
+ const nextSection = useCallback2(() => {
935
+ const currentIndex = SECTION_ORDER.indexOf(activeSection);
936
+ setSectionByIndex(currentIndex + 1);
937
+ }, [activeSection, setSectionByIndex]);
938
+ const previousSection = useCallback2(() => {
939
+ const currentIndex = SECTION_ORDER.indexOf(activeSection);
940
+ setSectionByIndex(currentIndex - 1);
941
+ }, [activeSection, setSectionByIndex]);
942
+ const setPanelByIndex = useCallback2((index) => {
943
+ const safeIndex = index < 0 ? FOCUS_ORDER.length - 1 : index >= FOCUS_ORDER.length ? 0 : index;
944
+ const panel = FOCUS_ORDER[safeIndex];
945
+ if (panel) {
946
+ setFocusedPanel(panel);
947
+ }
948
+ }, []);
949
+ const nextPanel = useCallback2(() => {
950
+ const currentIndex = FOCUS_ORDER.indexOf(focusedPanel);
951
+ setPanelByIndex(currentIndex + 1);
952
+ }, [focusedPanel, setPanelByIndex]);
953
+ const previousPanel = useCallback2(() => {
954
+ const currentIndex = FOCUS_ORDER.indexOf(focusedPanel);
955
+ setPanelByIndex(currentIndex - 1);
956
+ }, [focusedPanel, setPanelByIndex]);
957
+ const openExitConfirmation = useCallback2(() => {
958
+ setIsEscArmed(false);
959
+ setIsExitConfirmOpen(true);
960
+ }, []);
961
+ const closeExitConfirmation = useCallback2(() => {
962
+ setIsExitConfirmOpen(false);
963
+ setIsEscArmed(false);
964
+ }, []);
965
+ const armEscapeExit = useCallback2(() => {
966
+ setIsEscArmed(true);
967
+ setSystemMessages((previous) => {
968
+ const lastMessage = previous[previous.length - 1];
969
+ const message = "Press Esc again to open exit confirmation.";
970
+ if (lastMessage === message) {
971
+ return previous;
972
+ }
973
+ return [...previous, message];
974
+ });
975
+ }, []);
976
+ const isDataSourceSearchFocused = activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "search" && !isRunning && !isExitConfirmOpen;
977
+ const { submitInput } = useCommandInput({
978
+ busy: isRunning || isCatalogLoading,
979
+ input,
980
+ isInputFocused: focusedPanel === "input",
981
+ isModalOpen: isExitConfirmOpen,
982
+ hotkeysEnabled: !isRunning && !isDataSourceSearchFocused,
983
+ setInput,
984
+ onSubmit: executeLine,
985
+ onClear: () => {
986
+ setEntries([]);
987
+ setActiveEntryId(null);
988
+ setSystemMessages([]);
989
+ setSelectedHistoryIndex(0);
990
+ },
991
+ onShortcut: applyShortcut,
992
+ onNextSection: nextSection,
993
+ onPreviousSection: previousSection
994
+ });
995
+ useKeyboard2((keyEvent) => {
996
+ const keyName = keyEvent.name;
997
+ if (isExitConfirmOpen) {
998
+ if (isEscapeKey(keyName)) {
999
+ preventDefault2(keyEvent);
1000
+ closeExitConfirmation();
1001
+ return;
1002
+ }
1003
+ if (isEnterKey(keyName)) {
1004
+ preventDefault2(keyEvent);
1005
+ renderer.destroy();
1006
+ return;
1007
+ }
1008
+ if (keyEvent.ctrl && keyName === "c") {
1009
+ renderer.destroy();
1010
+ }
1011
+ return;
1012
+ }
1013
+ if (activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "search" && isEscapeKey(keyName)) {
1014
+ preventDefault2(keyEvent);
1015
+ setDataSourcePanelFocus("tree");
1016
+ return;
1017
+ }
1018
+ if (activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "tree" && isSlashKey(keyName)) {
1019
+ preventDefault2(keyEvent);
1020
+ setDataSourcePanelFocus("search");
1021
+ return;
1022
+ }
1023
+ if (keyEvent.ctrl && keyName === "c") {
1024
+ renderer.destroy();
1025
+ return;
1026
+ }
1027
+ if (isRunning) {
1028
+ return;
1029
+ }
1030
+ if (isEscapeKey(keyName)) {
1031
+ preventDefault2(keyEvent);
1032
+ if (focusedPanel !== "input") {
1033
+ setFocusedPanel("input");
1034
+ armEscapeExit();
1035
+ return;
1036
+ }
1037
+ if (!isEscArmed) {
1038
+ armEscapeExit();
1039
+ return;
1040
+ }
1041
+ openExitConfirmation();
1042
+ return;
1043
+ }
1044
+ if (activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "tree" && keyName === "space") {
1045
+ preventDefault2(keyEvent);
1046
+ if (selectedDataSourceRow?.kind === "app") {
1047
+ toggleAppExpansion(selectedDataSourceRow.appKey);
1048
+ }
1049
+ return;
1050
+ }
1051
+ if (keyName === "left" || keyName === "right") {
1052
+ if (focusedPanel === "input" && input.length > 0) {
1053
+ return;
1054
+ }
1055
+ preventDefault2(keyEvent);
1056
+ setIsEscArmed(false);
1057
+ if (keyName === "left") {
1058
+ previousPanel();
1059
+ } else {
1060
+ nextPanel();
1061
+ }
1062
+ return;
1063
+ }
1064
+ if (keyName === "r" && activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "tree" && !isCatalogLoading) {
1065
+ preventDefault2(keyEvent);
1066
+ void loadDataSourceCatalog();
1067
+ return;
1068
+ }
1069
+ if (keyName === "r" && activeSection === "history" && focusedPanel === "left" && input.length === 0 && !isRunning && !isCatalogLoading) {
1070
+ const entry = entries[selectedHistoryIndex];
1071
+ if (entry) {
1072
+ void runHistoryEntry(entry);
1073
+ }
1074
+ }
1075
+ });
1076
+ useEffect(() => {
1077
+ void executeArgs([], "(root)");
1078
+ }, [executeArgs]);
1079
+ useEffect(() => {
1080
+ if (!isEscArmed) {
1081
+ return;
1082
+ }
1083
+ const timer = setTimeout(() => {
1084
+ setIsEscArmed(false);
1085
+ }, ESC_ARM_TIMEOUT_MS);
1086
+ return () => {
1087
+ clearTimeout(timer);
1088
+ };
1089
+ }, [isEscArmed]);
1090
+ useEffect(() => {
1091
+ if (!isRunning && !isCatalogLoading) {
1092
+ return;
1093
+ }
1094
+ const timer = setInterval(() => {
1095
+ setSpinnerFrame((previous) => (previous + 1) % 4);
1096
+ }, 120);
1097
+ return () => {
1098
+ clearInterval(timer);
1099
+ };
1100
+ }, [isCatalogLoading, isRunning]);
1101
+ useEffect(() => {
1102
+ if (selectedHistoryIndex < entries.length) {
1103
+ return;
1104
+ }
1105
+ const nextIndex = Math.max(entries.length - 1, 0);
1106
+ setSelectedHistoryIndex(nextIndex);
1107
+ }, [entries.length, selectedHistoryIndex]);
1108
+ useEffect(() => {
1109
+ if (selectedDataSourceRowIndex < dataSourceRows.length) {
1110
+ return;
1111
+ }
1112
+ const nextIndex = Math.max(dataSourceRows.length - 1, 0);
1113
+ setSelectedDataSourceRowIndex(nextIndex);
1114
+ }, [dataSourceRows.length, selectedDataSourceRowIndex]);
1115
+ useEffect(() => {
1116
+ if (activeSection !== "datasources" && dataSourcePanelFocus !== "tree") {
1117
+ setDataSourcePanelFocus("tree");
1118
+ }
1119
+ }, [activeSection, dataSourcePanelFocus]);
1120
+ useEffect(() => {
1121
+ const scopeKey = `${environment?.id || ""}::${context?.tenantDisplay || ""}`;
1122
+ if (catalogScopeRef.current.length === 0) {
1123
+ catalogScopeRef.current = scopeKey;
1124
+ return;
1125
+ }
1126
+ if (catalogScopeRef.current === scopeKey) {
1127
+ return;
1128
+ }
1129
+ catalogScopeRef.current = scopeKey;
1130
+ if (catalogLoadedAt !== null) {
1131
+ setCatalogLoadedAt(null);
1132
+ setAppsCatalog([]);
1133
+ setDataSourcesCatalog([]);
1134
+ setExpandedAppKeys([]);
1135
+ setSelectedDataSourceRowIndex(0);
1136
+ setCatalogError(null);
1137
+ }
1138
+ }, [catalogLoadedAt, context?.tenantDisplay, environment?.id]);
1139
+ useEffect(() => {
1140
+ if (activeSection !== "datasources" || catalogLoadedAt !== null || isCatalogLoading) {
1141
+ return;
1142
+ }
1143
+ void loadDataSourceCatalog();
1144
+ }, [activeSection, catalogLoadedAt, isCatalogLoading, loadDataSourceCatalog]);
1145
+ const spinner = useMemo(() => {
1146
+ return ["|", "/", "-", "\\"][spinnerFrame] || "|";
1147
+ }, [spinnerFrame]);
1148
+ const activeEntry = useMemo(() => {
1149
+ if (activeEntryId) {
1150
+ const found = entries.find((entry) => entry.id === activeEntryId);
1151
+ if (found) {
1152
+ return found;
1153
+ }
1154
+ }
1155
+ return entries[0] || null;
1156
+ }, [activeEntryId, entries]);
1157
+ const sectionOptions = useMemo(() => {
1158
+ const ordered = [
1159
+ activeSection,
1160
+ ...SECTION_ORDER.filter((section) => section !== activeSection)
1161
+ ];
1162
+ return ordered.map((section) => ({
1163
+ name: section === "shortcuts" ? "Shortcuts" : section === "datasources" ? "DataSources" : section === "history" ? "History" : section === "messages" ? "Messages" : "Help",
1164
+ description: section === "shortcuts" ? "Quick actions" : section === "datasources" ? "App tree" : section === "history" ? "Past runs" : section === "messages" ? "System output" : "Key map",
1165
+ value: section
1166
+ }));
1167
+ }, [activeSection]);
1168
+ const shortcutOptions = useMemo(() => {
1169
+ return SHORTCUTS.map((shortcut) => ({
1170
+ name: `${shortcut.id}. ${shortcut.label}`,
1171
+ description: shortcut.run || shortcut.template || "",
1172
+ value: shortcut.id
1173
+ }));
1174
+ }, []);
1175
+ const historyOptions = useMemo(() => {
1176
+ return entries.map((entry) => ({
1177
+ name: formatHistoryLabel(entry),
1178
+ description: new Date(entry.createdAt).toLocaleTimeString(),
1179
+ value: entry.id
1180
+ }));
1181
+ }, [entries]);
1182
+ const dataSourceOptions = useMemo(() => {
1183
+ return dataSourceRows.map((row) => {
1184
+ if (row.kind === "app") {
1185
+ const itemCount = dataSourceCountByAppKey.get(row.appKey) || 0;
1186
+ return {
1187
+ name: `${row.expanded ? "[-]" : "[+]"} ${row.appName}${row.appSlug ? ` (${row.appSlug})` : ""}`,
1188
+ description: `${itemCount} data source${itemCount === 1 ? "" : "s"}`,
1189
+ value: row.id
1190
+ };
1191
+ }
1192
+ const dataSourceLabel = row.dataSourceName || row.dataSourceSlug || row.dataSourceId || "Unknown";
1193
+ return {
1194
+ name: ` - ${dataSourceLabel}${row.dataSourceSlug ? ` (${row.dataSourceSlug})` : ""}`,
1195
+ description: row.appSlug ? `app: ${row.appSlug}` : "missing app slug",
1196
+ value: row.id
1197
+ };
1198
+ });
1199
+ }, [dataSourceCountByAppKey, dataSourceRows]);
1200
+ const renderLeftSection = () => {
1201
+ if (activeSection === "shortcuts") {
1202
+ return /* @__PURE__ */ jsx2(
1203
+ "select",
1204
+ {
1205
+ focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning,
1206
+ options: shortcutOptions,
1207
+ selectedIndex: selectedShortcutIndex,
1208
+ onChange: (index) => {
1209
+ setSelectedShortcutIndex(index);
1210
+ },
1211
+ onSelect: (index, option) => {
1212
+ setSelectedShortcutIndex(index);
1213
+ const shortcutId = Number(option?.value || 0);
1214
+ if (Number.isFinite(shortcutId) && shortcutId > 0) {
1215
+ applyShortcut(shortcutId);
1216
+ }
1217
+ },
1218
+ showScrollIndicator: true,
1219
+ style: {
1220
+ flexGrow: 1
1221
+ }
1222
+ }
1223
+ );
1224
+ }
1225
+ if (activeSection === "datasources") {
1226
+ const treeFocused = focusedPanel === "left" && dataSourcePanelFocus === "tree" && !isRunning && !isExitConfirmOpen;
1227
+ const searchFocused = focusedPanel === "left" && dataSourcePanelFocus === "search" && !isRunning && !isExitConfirmOpen;
1228
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", flexGrow: 1, children: [
1229
+ /* @__PURE__ */ jsx2("box", { border: true, borderStyle: "single", borderColor: searchFocused ? "#7aa2f7" : "#414868", paddingX: 1, children: /* @__PURE__ */ jsx2(
1230
+ "input",
1231
+ {
1232
+ value: dataSourceSearch,
1233
+ placeholder: "Search data sources...",
1234
+ onChange: (value) => {
1235
+ setDataSourceSearch(value);
1236
+ setSelectedDataSourceRowIndex(0);
1237
+ },
1238
+ onSubmit: () => {
1239
+ setDataSourcePanelFocus("tree");
1240
+ },
1241
+ focused: searchFocused,
1242
+ style: {
1243
+ flexGrow: 1
1244
+ }
1245
+ }
1246
+ ) }),
1247
+ /* @__PURE__ */ jsx2("box", { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx2("text", { fg: "gray", children: "/ to search, Esc to tree, Space toggle app, Enter run ds get, r refresh" }) }),
1248
+ isCatalogLoading && /* @__PURE__ */ jsx2("text", { fg: "cyan", children: "Loading DataSources catalog..." }),
1249
+ catalogError && /* @__PURE__ */ jsx2("text", { fg: "red", children: catalogError }),
1250
+ dataSourceOptions.length === 0 ? /* @__PURE__ */ jsx2("text", { fg: "gray", children: dataSourceSearch.trim().length > 0 ? `No data sources match "${dataSourceSearch}".` : "No data sources available for this tenant." }) : /* @__PURE__ */ jsx2(
1251
+ "select",
1252
+ {
1253
+ focused: treeFocused,
1254
+ options: dataSourceOptions,
1255
+ selectedIndex: selectedDataSourceRowIndex,
1256
+ onChange: (index) => {
1257
+ setSelectedDataSourceRowIndex(index);
1258
+ },
1259
+ onSelect: (index) => {
1260
+ setSelectedDataSourceRowIndex(index);
1261
+ const row = dataSourceRows[index] || null;
1262
+ void runSelectedDataSourceRow(row);
1263
+ },
1264
+ showScrollIndicator: true,
1265
+ style: {
1266
+ flexGrow: 1
1267
+ }
1268
+ }
1269
+ )
1270
+ ] });
1271
+ }
1272
+ if (activeSection === "history") {
1273
+ if (entries.length === 0) {
1274
+ return /* @__PURE__ */ jsx2("text", { fg: "gray", children: "No command history yet." });
1275
+ }
1276
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", flexGrow: 1, children: [
1277
+ /* @__PURE__ */ jsx2("text", { fg: "cyan", children: "Press `r` to re-run selected command." }),
1278
+ /* @__PURE__ */ jsx2(
1279
+ "select",
1280
+ {
1281
+ focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning,
1282
+ options: historyOptions,
1283
+ selectedIndex: selectedHistoryIndex,
1284
+ onChange: (index, option) => {
1285
+ setSelectedHistoryIndex(index);
1286
+ if (typeof option?.value === "string") {
1287
+ setActiveEntryId(option.value);
1288
+ }
1289
+ },
1290
+ onSelect: (index, option) => {
1291
+ setSelectedHistoryIndex(index);
1292
+ if (typeof option?.value === "string") {
1293
+ setActiveEntryId(option.value);
1294
+ }
1295
+ },
1296
+ showScrollIndicator: true,
1297
+ style: {
1298
+ flexGrow: 1
1299
+ }
1300
+ }
1301
+ )
1302
+ ] });
1303
+ }
1304
+ if (activeSection === "messages") {
1305
+ return /* @__PURE__ */ jsxs2("scrollbox", { focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning, style: { height: "100%" }, children: [
1306
+ systemMessages.length === 0 && /* @__PURE__ */ jsx2("text", { fg: "gray", children: "No messages yet." }),
1307
+ systemMessages.map((line, index) => /* @__PURE__ */ jsx2("text", { children: line }, `message-${index}`))
1308
+ ] });
1309
+ }
1310
+ return /* @__PURE__ */ jsxs2("scrollbox", { focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning, style: { height: "100%" }, children: [
1311
+ /* @__PURE__ */ jsx2("text", { fg: "cyan", children: "Key Bindings" }),
1312
+ /* @__PURE__ */ jsx2("text", { children: "Left/Right: switch focused panel" }),
1313
+ /* @__PURE__ */ jsx2("text", { children: "Up/Down in left lists: selection" }),
1314
+ /* @__PURE__ */ jsx2("text", { children: "Up/Down in result: scroll" }),
1315
+ /* @__PURE__ */ jsx2("text", { children: "Enter: run command" }),
1316
+ /* @__PURE__ */ jsx2("text", { children: "Up/Down in input: command history" }),
1317
+ /* @__PURE__ */ jsx2("text", { children: "Tab / Shift+Tab: next/prev left section" }),
1318
+ /* @__PURE__ */ jsx2("text", { children: "[ / ]: next/prev left section" }),
1319
+ /* @__PURE__ */ jsx2("text", { children: "1..6: run shortcut" }),
1320
+ /* @__PURE__ */ jsx2("text", { children: "/ in DataSources: focus search" }),
1321
+ /* @__PURE__ */ jsx2("text", { children: "Esc in DataSources search: return to tree" }),
1322
+ /* @__PURE__ */ jsx2("text", { children: "Space in DataSources tree: toggle app folder" }),
1323
+ /* @__PURE__ */ jsx2("text", { children: "Enter in DataSources tree: run ds get or toggle app" }),
1324
+ /* @__PURE__ */ jsx2("text", { children: "Ctrl+L: clear command and message history" }),
1325
+ /* @__PURE__ */ jsx2("text", { children: "Esc (other panel): focus command input" }),
1326
+ /* @__PURE__ */ jsx2("text", { children: "Esc then Esc: open exit confirmation" }),
1327
+ /* @__PURE__ */ jsx2("text", { children: "Enter (confirm): exit, Esc (confirm): cancel" }),
1328
+ /* @__PURE__ */ jsx2("text", { children: "Ctrl+C: force quit" })
1329
+ ] });
1330
+ };
1331
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", width: "100%", height: "100%", padding: 1, children: [
1332
+ /* @__PURE__ */ jsxs2("box", { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, paddingX: 1, children: [
1333
+ /* @__PURE__ */ jsx2("ascii-font", { text: "DOCYRUS", font: "tiny", color: "#7aa2f7" }),
1334
+ /* @__PURE__ */ jsxs2("box", { flexDirection: "column", alignItems: "flex-end", justifyContent: "center", children: [
1335
+ /* @__PURE__ */ jsxs2("text", { children: [
1336
+ "env: ",
1337
+ environment ? `${environment.name} (${environment.id})` : "unknown"
1338
+ ] }),
1339
+ /* @__PURE__ */ jsxs2("text", { children: [
1340
+ "status: ",
1341
+ isRunning ? `${spinner} running` : isCatalogLoading ? `${spinner} loading-catalog` : "idle"
1342
+ ] }),
1343
+ /* @__PURE__ */ jsxs2("text", { children: [
1344
+ "account: ",
1345
+ context ? context.email : "not-authenticated"
1346
+ ] }),
1347
+ /* @__PURE__ */ jsxs2("text", { children: [
1348
+ "tenant: ",
1349
+ context ? context.tenantDisplay : "-"
1350
+ ] })
1351
+ ] })
1352
+ ] }),
1353
+ /* @__PURE__ */ jsxs2("box", { flexDirection: "row", flexGrow: 1, children: [
1354
+ /* @__PURE__ */ jsxs2(
1355
+ "box",
1356
+ {
1357
+ width: 46,
1358
+ marginRight: 1,
1359
+ border: true,
1360
+ borderStyle: "rounded",
1361
+ borderColor: focusedPanel === "left" ? "#7aa2f7" : "#3b4261",
1362
+ padding: 1,
1363
+ flexDirection: "column",
1364
+ children: [
1365
+ /* @__PURE__ */ jsx2(
1366
+ "tab-select",
1367
+ {
1368
+ focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning,
1369
+ options: sectionOptions,
1370
+ onChange: (_index, option) => {
1371
+ if (typeof option?.value === "string") {
1372
+ setActiveSection(option.value);
1373
+ setDataSourcePanelFocus("tree");
1374
+ }
1375
+ }
1376
+ }
1377
+ ),
1378
+ /* @__PURE__ */ jsx2("box", { marginTop: 1, flexGrow: 1, border: true, borderStyle: "single", borderColor: "#414868", padding: 1, children: renderLeftSection() })
1379
+ ]
1380
+ }
1381
+ ),
1382
+ /* @__PURE__ */ jsx2(
1383
+ "box",
1384
+ {
1385
+ border: true,
1386
+ borderStyle: "double",
1387
+ borderColor: focusedPanel === "result" ? "#9ece6a" : "#3b4261",
1388
+ padding: 1,
1389
+ flexGrow: 1,
1390
+ children: isRunning ? /* @__PURE__ */ jsxs2("box", { width: "100%", height: "100%", alignItems: "center", justifyContent: "center", flexDirection: "column", children: [
1391
+ /* @__PURE__ */ jsx2("text", { fg: "cyan", children: spinner }),
1392
+ /* @__PURE__ */ jsx2("text", { fg: "gray", children: "Running command..." })
1393
+ ] }) : renderResult({
1394
+ entry: activeEntry,
1395
+ isFocused: focusedPanel === "result" && !isExitConfirmOpen
1396
+ })
1397
+ }
1398
+ )
1399
+ ] }),
1400
+ /* @__PURE__ */ jsxs2(
1401
+ "box",
1402
+ {
1403
+ border: true,
1404
+ borderStyle: "rounded",
1405
+ borderColor: focusedPanel === "input" ? "#e0af68" : "#3b4261",
1406
+ padding: 1,
1407
+ marginTop: 1,
1408
+ children: [
1409
+ /* @__PURE__ */ jsx2("text", { fg: "cyan", children: "> " }),
1410
+ /* @__PURE__ */ jsx2(
1411
+ "input",
1412
+ {
1413
+ style: { flexGrow: 1 },
1414
+ value: input,
1415
+ placeholder: "Type a command...",
1416
+ onChange: (value) => {
1417
+ setInput(value);
1418
+ },
1419
+ onSubmit: (value) => {
1420
+ submitInput(typeof value === "string" ? value : void 0);
1421
+ },
1422
+ focused: !isRunning && !isCatalogLoading && focusedPanel === "input" && !isExitConfirmOpen
1423
+ }
1424
+ )
1425
+ ]
1426
+ }
1427
+ ),
1428
+ isExitConfirmOpen && /* @__PURE__ */ jsx2(
1429
+ "box",
1430
+ {
1431
+ style: {
1432
+ position: "absolute",
1433
+ top: 0,
1434
+ left: 0,
1435
+ width: "100%",
1436
+ height: "100%"
1437
+ },
1438
+ alignItems: "center",
1439
+ justifyContent: "center",
1440
+ children: /* @__PURE__ */ jsxs2(
1441
+ "box",
1442
+ {
1443
+ width: 58,
1444
+ padding: 1,
1445
+ border: true,
1446
+ borderStyle: "double",
1447
+ borderColor: "#f7768e",
1448
+ flexDirection: "column",
1449
+ style: {
1450
+ backgroundColor: "#1f2335"
1451
+ },
1452
+ children: [
1453
+ /* @__PURE__ */ jsx2("text", { fg: "#f7768e", children: "Exit Docyrus TUI?" }),
1454
+ /* @__PURE__ */ jsx2("text", { children: "Press Enter to exit." }),
1455
+ /* @__PURE__ */ jsx2("text", { children: "Press Esc to stay in this session." })
1456
+ ]
1457
+ }
1458
+ )
1459
+ }
1460
+ )
1461
+ ] });
1462
+ }
1463
+
1464
+ // src/tui/opentuiMain.tsx
1465
+ import { jsx as jsx3 } from "@opentui/react/jsx-runtime";
1466
+ function readRequiredEnv(name) {
1467
+ const value = process.env[name];
1468
+ if (!value || value.trim().length === 0) {
1469
+ throw new Error(`Missing required launch variable: ${name}`);
1470
+ }
1471
+ return value;
1472
+ }
1473
+ function readLaunchConfig() {
1474
+ return {
1475
+ version: process.env.DOCYRUS_TUI_VERSION || "dev",
1476
+ executionConfig: {
1477
+ executable: readRequiredEnv("DOCYRUS_TUI_EXECUTABLE"),
1478
+ scriptPath: readRequiredEnv("DOCYRUS_TUI_SCRIPT")
1479
+ }
1480
+ };
1481
+ }
1482
+ async function startTui() {
1483
+ const launchConfig = readLaunchConfig();
1484
+ const renderer = await createCliRenderer({
1485
+ exitOnCtrlC: false
1486
+ });
1487
+ const root = createRoot(renderer);
1488
+ root.render(
1489
+ /* @__PURE__ */ jsx3(
1490
+ DocyrusOpenTuiApp,
1491
+ {
1492
+ executionConfig: launchConfig.executionConfig,
1493
+ version: launchConfig.version
1494
+ }
1495
+ )
1496
+ );
1497
+ }
1498
+ void startTui().catch((error) => {
1499
+ const message = error instanceof Error ? error.message : String(error);
1500
+ console.error(`Failed to start Docyrus OpenTUI: ${message}`);
1501
+ process.exitCode = 1;
1502
+ });
1503
+ //# sourceMappingURL=tui.mjs.map