pilotswarm-cli 0.1.13 → 0.1.15
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 +3 -0
- package/bin/tui.js +5 -2
- package/package.json +4 -3
- package/src/app.js +55 -0
- package/src/bootstrap-env.js +1 -37
- package/src/index.js +67 -0
- package/src/node-sdk-transport.js +422 -61
- package/src/platform.js +90 -35
- package/src/plugin-config.js +239 -0
- package/src/portal.js +7 -0
package/src/platform.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
|
-
import { parseTerminalMarkupRuns } from "pilotswarm-ui-core";
|
|
4
|
+
import { DEFAULT_THEME_ID, getTheme, isThemeLight, parseTerminalMarkupRuns } from "pilotswarm-ui-core";
|
|
5
5
|
|
|
6
6
|
const MAX_PROMPT_INPUT_ROWS = 3;
|
|
7
|
-
const SELECTION_BACKGROUND = "
|
|
8
|
-
const SELECTION_FOREGROUND = "
|
|
7
|
+
const SELECTION_BACKGROUND = "selectionBackground";
|
|
8
|
+
const SELECTION_FOREGROUND = "selectionForeground";
|
|
9
9
|
|
|
10
10
|
function clampValue(value, min, max) {
|
|
11
11
|
return Math.max(min, Math.min(max, value));
|
|
@@ -25,8 +25,27 @@ const tuiPlatformRuntime = {
|
|
|
25
25
|
paneRegistry: new Map(),
|
|
26
26
|
selection: createEmptySelection(),
|
|
27
27
|
renderInvalidator: null,
|
|
28
|
+
themeId: DEFAULT_THEME_ID,
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
function getCurrentTheme() {
|
|
32
|
+
return getTheme(tuiPlatformRuntime.themeId) || getTheme(DEFAULT_THEME_ID);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveColorToken(color) {
|
|
36
|
+
if (!color) return undefined;
|
|
37
|
+
const theme = getCurrentTheme();
|
|
38
|
+
return theme?.tui?.[color] || color;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function shouldDimGrayTextForTheme(theme = getCurrentTheme()) {
|
|
42
|
+
return !isThemeLight(theme);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isDimColorToken(color) {
|
|
46
|
+
return color === "gray" && shouldDimGrayTextForTheme();
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
function trimText(value, width) {
|
|
31
50
|
if (width <= 0) return "";
|
|
32
51
|
const text = String(value || "");
|
|
@@ -230,11 +249,11 @@ function flattenTitleText(title) {
|
|
|
230
249
|
function renderInlineRuns(runs, keyPrefix = "run") {
|
|
231
250
|
return runs.map((run, index) => React.createElement(Text, {
|
|
232
251
|
key: `${keyPrefix}:${index}`,
|
|
233
|
-
color: run.color
|
|
234
|
-
backgroundColor: run.backgroundColor
|
|
252
|
+
color: resolveColorToken(run.color),
|
|
253
|
+
backgroundColor: resolveColorToken(run.backgroundColor),
|
|
235
254
|
bold: Boolean(run.bold),
|
|
236
255
|
underline: Boolean(run.underline),
|
|
237
|
-
dimColor: run.color
|
|
256
|
+
dimColor: isDimColorToken(run.color),
|
|
238
257
|
}, run.text || ""));
|
|
239
258
|
}
|
|
240
259
|
|
|
@@ -339,31 +358,58 @@ function renderPanelRow(line, rowKey, contentWidth, borderColor, scrollIndicator
|
|
|
339
358
|
const selectedRuns = applySelectionToRuns(lineToRuns(line, contentWidth), normalizedSelection);
|
|
340
359
|
|
|
341
360
|
return React.createElement(Box, { key: `row:${rowKey}`, flexDirection: "row" },
|
|
342
|
-
React.createElement(Text, { color: borderColor }, "│ "),
|
|
343
|
-
React.createElement(Box, { width: contentWidth, backgroundColor: fillColor
|
|
361
|
+
React.createElement(Text, { color: resolveColorToken(borderColor) }, "│ "),
|
|
362
|
+
React.createElement(Box, { width: contentWidth, backgroundColor: resolveColorToken(fillColor) },
|
|
344
363
|
!line
|
|
345
364
|
? React.createElement(Text, null, " ".repeat(contentWidth))
|
|
346
365
|
: selectedRuns.length > 0
|
|
347
366
|
? renderInlineRuns(selectedRuns, `inline:${rowKey}`)
|
|
348
367
|
: React.createElement(Text, null, "")),
|
|
349
|
-
React.createElement(Text, {
|
|
350
|
-
|
|
368
|
+
React.createElement(Text, {
|
|
369
|
+
color: scrollIndicator ? resolveColorToken("gray") : undefined,
|
|
370
|
+
dimColor: Boolean(scrollIndicator) && shouldDimGrayTextForTheme(),
|
|
371
|
+
}, scrollChar),
|
|
372
|
+
React.createElement(Text, { color: resolveColorToken(borderColor) }, "│"));
|
|
351
373
|
}
|
|
352
374
|
|
|
353
|
-
function renderBorderTop(title, color, width) {
|
|
375
|
+
function renderBorderTop(title, color, width, titleRight = null) {
|
|
354
376
|
const safeWidth = Math.max(8, Number(width) || 40);
|
|
355
|
-
const
|
|
356
|
-
const
|
|
377
|
+
const normalizedTitleRuns = normalizeTitleRuns(title, color);
|
|
378
|
+
const normalizedRightRuns = titleRight ? normalizeTitleRuns(titleRight, "gray") : [];
|
|
379
|
+
let safeTitleRuns = trimRuns(normalizedTitleRuns, Math.max(1, safeWidth - 6));
|
|
380
|
+
let safeRightRuns = trimRuns(normalizedRightRuns, Math.max(0, safeWidth - 8));
|
|
381
|
+
let titleWidth = titleRunLength(safeTitleRuns);
|
|
382
|
+
let rightWidth = titleRunLength(safeRightRuns);
|
|
383
|
+
const hasRightTitle = rightWidth > 0;
|
|
384
|
+
const chromeWidth = hasRightTitle ? 6 : 5;
|
|
385
|
+
const availableTitleWidth = Math.max(1, safeWidth - chromeWidth);
|
|
386
|
+
|
|
387
|
+
if (titleWidth + rightWidth > availableTitleWidth) {
|
|
388
|
+
safeTitleRuns = trimRuns(normalizedTitleRuns, Math.max(1, availableTitleWidth - rightWidth));
|
|
389
|
+
titleWidth = titleRunLength(safeTitleRuns);
|
|
390
|
+
if (titleWidth + rightWidth > availableTitleWidth) {
|
|
391
|
+
safeRightRuns = trimRuns(normalizedRightRuns, Math.max(0, availableTitleWidth - titleWidth));
|
|
392
|
+
rightWidth = titleRunLength(safeRightRuns);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const fill = Math.max(0, safeWidth - titleWidth - rightWidth - chromeWidth);
|
|
357
397
|
|
|
358
398
|
return React.createElement(Box, null,
|
|
359
|
-
React.createElement(Text, { color }, "╭─ "),
|
|
399
|
+
React.createElement(Text, { color: resolveColorToken(color) }, "╭─ "),
|
|
360
400
|
renderInlineRuns(safeTitleRuns, "title"),
|
|
361
|
-
|
|
401
|
+
hasRightTitle
|
|
402
|
+
? [
|
|
403
|
+
React.createElement(Text, { key: "title-fill", color: resolveColorToken(color) }, ` ${"─".repeat(fill)} `),
|
|
404
|
+
...renderInlineRuns(safeRightRuns, "title-right"),
|
|
405
|
+
React.createElement(Text, { key: "title-end", color: resolveColorToken(color) }, "╮"),
|
|
406
|
+
]
|
|
407
|
+
: React.createElement(Text, { color: resolveColorToken(color) }, ` ${"─".repeat(fill)}╮`));
|
|
362
408
|
}
|
|
363
409
|
|
|
364
410
|
function renderBorderBottom(color, width) {
|
|
365
411
|
const safeWidth = Math.max(8, Number(width) || 40);
|
|
366
|
-
return React.createElement(Text, { color }, `╰${"─".repeat(Math.max(0, safeWidth - 2))}╯`);
|
|
412
|
+
return React.createElement(Text, { color: resolveColorToken(color) }, `╰${"─".repeat(Math.max(0, safeWidth - 2))}╯`);
|
|
367
413
|
}
|
|
368
414
|
|
|
369
415
|
function compareSelectionPoints(left, right) {
|
|
@@ -527,10 +573,10 @@ function linesToElements(lines) {
|
|
|
527
573
|
}
|
|
528
574
|
return React.createElement(Text, {
|
|
529
575
|
key: `text:${index}`,
|
|
530
|
-
color: line.color
|
|
531
|
-
backgroundColor: line.backgroundColor
|
|
576
|
+
color: resolveColorToken(line.color),
|
|
577
|
+
backgroundColor: resolveColorToken(line.backgroundColor),
|
|
532
578
|
bold: Boolean(line.bold),
|
|
533
|
-
dimColor: line.color
|
|
579
|
+
dimColor: isDimColorToken(line.color),
|
|
534
580
|
}, line.text || "");
|
|
535
581
|
});
|
|
536
582
|
}
|
|
@@ -540,6 +586,7 @@ function Root({ children }) {
|
|
|
540
586
|
flexDirection: "column",
|
|
541
587
|
height: process.stdout.rows || 40,
|
|
542
588
|
width: process.stdout.columns || 120,
|
|
589
|
+
backgroundColor: resolveColorToken("background"),
|
|
543
590
|
}, children);
|
|
544
591
|
}
|
|
545
592
|
|
|
@@ -554,17 +601,18 @@ function Column({ children, ...props }) {
|
|
|
554
601
|
function Header({ title, subtitle }) {
|
|
555
602
|
return React.createElement(Box, {
|
|
556
603
|
borderStyle: "round",
|
|
557
|
-
borderColor: "cyan",
|
|
604
|
+
borderColor: resolveColorToken("cyan"),
|
|
558
605
|
paddingX: 1,
|
|
559
606
|
marginBottom: 1,
|
|
560
607
|
justifyContent: "space-between",
|
|
561
608
|
},
|
|
562
|
-
React.createElement(Text, { bold: true, color: "cyan" }, title),
|
|
563
|
-
React.createElement(Text, { color: "gray" }, subtitle || ""));
|
|
609
|
+
React.createElement(Text, { bold: true, color: resolveColorToken("cyan") }, title),
|
|
610
|
+
React.createElement(Text, { color: resolveColorToken("gray"), dimColor: shouldDimGrayTextForTheme() }, subtitle || ""));
|
|
564
611
|
}
|
|
565
612
|
|
|
566
613
|
function Panel({
|
|
567
614
|
title,
|
|
615
|
+
titleRight,
|
|
568
616
|
color = "white",
|
|
569
617
|
focused = false,
|
|
570
618
|
width,
|
|
@@ -633,7 +681,7 @@ function Panel({
|
|
|
633
681
|
flexGrow,
|
|
634
682
|
flexBasis,
|
|
635
683
|
},
|
|
636
|
-
renderBorderTop(title, borderColor, safeWidth),
|
|
684
|
+
renderBorderTop(title, borderColor, safeWidth, titleRight),
|
|
637
685
|
lines
|
|
638
686
|
? React.createElement(Box, { flexDirection: "column", flexGrow: 1, backgroundColor: fillColor || undefined },
|
|
639
687
|
[
|
|
@@ -660,10 +708,10 @@ function Panel({
|
|
|
660
708
|
: React.createElement(Box, {
|
|
661
709
|
flexDirection: "column",
|
|
662
710
|
borderStyle: "round",
|
|
663
|
-
borderColor: borderColor,
|
|
711
|
+
borderColor: resolveColorToken(borderColor),
|
|
664
712
|
paddingX: 1,
|
|
665
713
|
flexGrow: 1,
|
|
666
|
-
backgroundColor: fillColor
|
|
714
|
+
backgroundColor: resolveColorToken(fillColor),
|
|
667
715
|
}, children),
|
|
668
716
|
renderBorderBottom(borderColor, safeWidth));
|
|
669
717
|
}
|
|
@@ -717,11 +765,11 @@ function renderPromptRow(lineText, cursorColumn, { color, showCursor, keyPrefix,
|
|
|
717
765
|
showCursor
|
|
718
766
|
? cursorChar
|
|
719
767
|
? React.createElement(Text, {
|
|
720
|
-
color: "
|
|
721
|
-
backgroundColor: "
|
|
768
|
+
color: resolveColorToken("promptCursorForeground"),
|
|
769
|
+
backgroundColor: resolveColorToken("promptCursorBackground"),
|
|
722
770
|
dimColor,
|
|
723
771
|
}, cursorChar)
|
|
724
|
-
: React.createElement(Text, { color: "
|
|
772
|
+
: React.createElement(Text, { color: resolveColorToken("promptCursorBackground") }, "█")
|
|
725
773
|
: null,
|
|
726
774
|
after ? React.createElement(Text, { color, dimColor }, after) : null,
|
|
727
775
|
);
|
|
@@ -732,7 +780,7 @@ function Input({ label, value, focused, placeholder, rows = 1, cursorIndex = 0 }
|
|
|
732
780
|
const isEmpty = safeValue.length === 0;
|
|
733
781
|
const safeRows = clampValue(Number(rows) || 1, 1, MAX_PROMPT_INPUT_ROWS);
|
|
734
782
|
const labelPrefix = React.createElement(Text, {
|
|
735
|
-
color: focused ? "red" : "green",
|
|
783
|
+
color: resolveColorToken(focused ? "red" : "green"),
|
|
736
784
|
bold: true,
|
|
737
785
|
}, `${label}: `);
|
|
738
786
|
const cursorPosition = getPromptCursorPosition(safeValue, cursorIndex);
|
|
@@ -756,8 +804,8 @@ function Input({ label, value, focused, placeholder, rows = 1, cursorIndex = 0 }
|
|
|
756
804
|
isEmpty
|
|
757
805
|
? [
|
|
758
806
|
renderPromptRow(placeholder || "Type a message and press Enter", focused ? 0 : null, {
|
|
759
|
-
color: "gray",
|
|
760
|
-
dimColor:
|
|
807
|
+
color: resolveColorToken("gray"),
|
|
808
|
+
dimColor: shouldDimGrayTextForTheme(),
|
|
761
809
|
showCursor: Boolean(focused),
|
|
762
810
|
keyPrefix: "prompt-line:0",
|
|
763
811
|
prefix: labelPrefix,
|
|
@@ -768,7 +816,7 @@ function Input({ label, value, focused, placeholder, rows = 1, cursorIndex = 0 }
|
|
|
768
816
|
}, React.createElement(Text, null, ""))),
|
|
769
817
|
]
|
|
770
818
|
: displayLines.map((line, index) => renderPromptRow(line, focused && visibleCursorLine === index ? cursorPosition.column : null, {
|
|
771
|
-
color: "white",
|
|
819
|
+
color: resolveColorToken("white"),
|
|
772
820
|
showCursor: Boolean(focused && visibleCursorLine === index),
|
|
773
821
|
keyPrefix: `prompt-line:${index}`,
|
|
774
822
|
prefix: index === 0 ? labelPrefix : null,
|
|
@@ -779,12 +827,12 @@ function Input({ label, value, focused, placeholder, rows = 1, cursorIndex = 0 }
|
|
|
779
827
|
function StatusLine({ left, right }) {
|
|
780
828
|
return React.createElement(Box, {
|
|
781
829
|
borderStyle: "round",
|
|
782
|
-
borderColor: "gray",
|
|
830
|
+
borderColor: resolveColorToken("gray"),
|
|
783
831
|
paddingX: 1,
|
|
784
832
|
justifyContent: "space-between",
|
|
785
833
|
},
|
|
786
|
-
React.createElement(Text, { color: "white" }, left || ""),
|
|
787
|
-
React.createElement(Text, { color: "gray", dimColor:
|
|
834
|
+
React.createElement(Text, { color: resolveColorToken("white") }, left || ""),
|
|
835
|
+
React.createElement(Text, { color: resolveColorToken("gray"), dimColor: shouldDimGrayTextForTheme() }, right || ""));
|
|
788
836
|
}
|
|
789
837
|
|
|
790
838
|
function Overlay({ children }) {
|
|
@@ -807,6 +855,7 @@ export function createTuiPlatform() {
|
|
|
807
855
|
tuiPlatformRuntime.paneRegistry.clear();
|
|
808
856
|
tuiPlatformRuntime.selection = createEmptySelection();
|
|
809
857
|
tuiPlatformRuntime.renderInvalidator = null;
|
|
858
|
+
tuiPlatformRuntime.themeId = DEFAULT_THEME_ID;
|
|
810
859
|
|
|
811
860
|
return {
|
|
812
861
|
Root,
|
|
@@ -818,6 +867,12 @@ export function createTuiPlatform() {
|
|
|
818
867
|
Lines,
|
|
819
868
|
Input,
|
|
820
869
|
StatusLine,
|
|
870
|
+
setTheme(themeId) {
|
|
871
|
+
const nextTheme = getTheme(themeId) || getTheme(DEFAULT_THEME_ID);
|
|
872
|
+
if (!nextTheme || nextTheme.id === tuiPlatformRuntime.themeId) return;
|
|
873
|
+
tuiPlatformRuntime.themeId = nextTheme.id;
|
|
874
|
+
requestTuiRender();
|
|
875
|
+
},
|
|
821
876
|
setRenderInvalidator(fn) {
|
|
822
877
|
tuiPlatformRuntime.renderInvalidator = typeof fn === "function" ? fn : null;
|
|
823
878
|
},
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const pkgRoot = path.resolve(__dirname, "..");
|
|
7
|
+
const defaultTuiSplashPath = path.join(pkgRoot, "tui-splash.txt");
|
|
8
|
+
|
|
9
|
+
function fileExists(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
return fs.existsSync(filePath);
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readOptionalTextFile(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(filePath, "utf-8").trimEnd();
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getObject(value) {
|
|
26
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
27
|
+
? value
|
|
28
|
+
: {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function firstNonEmptyString(...values) {
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
if (typeof value === "string" && value.trim()) {
|
|
34
|
+
return value.trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveRelativePath(baseDir, relativePath) {
|
|
41
|
+
if (!baseDir || typeof relativePath !== "string" || !relativePath.trim()) return null;
|
|
42
|
+
const basePath = path.resolve(baseDir);
|
|
43
|
+
const filePath = path.resolve(basePath, relativePath);
|
|
44
|
+
if (filePath !== basePath && !filePath.startsWith(`${basePath}${path.sep}`)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return filePath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readRelativeTextFile(baseDir, relativePath) {
|
|
51
|
+
const filePath = resolveRelativePath(baseDir, relativePath);
|
|
52
|
+
if (!filePath) return null;
|
|
53
|
+
return readOptionalTextFile(filePath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveRelativeAssetFile(baseDir, relativePath) {
|
|
57
|
+
const filePath = resolveRelativePath(baseDir, relativePath);
|
|
58
|
+
if (!filePath || !fileExists(filePath)) return null;
|
|
59
|
+
return filePath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function firstAssetUrl(...values) {
|
|
63
|
+
for (const value of values) {
|
|
64
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
if (/^(https?:\/\/|\/|data:|blob:)/iu.test(trimmed)) {
|
|
67
|
+
return trimmed;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolvePortalAsset(baseDir, { file, url }) {
|
|
74
|
+
const directUrl = firstAssetUrl(url);
|
|
75
|
+
if (directUrl) {
|
|
76
|
+
return { filePath: null, publicUrl: directUrl };
|
|
77
|
+
}
|
|
78
|
+
const filePath = resolveRelativeAssetFile(baseDir, file);
|
|
79
|
+
if (!filePath) {
|
|
80
|
+
return { filePath: null, publicUrl: null };
|
|
81
|
+
}
|
|
82
|
+
return { filePath, publicUrl: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readSplashValue(baseDir, config, fallback) {
|
|
86
|
+
if (typeof config?.splash === "string" && config.splash.trim()) {
|
|
87
|
+
return config.splash;
|
|
88
|
+
}
|
|
89
|
+
if (typeof config?.splashFile === "string" && config.splashFile.trim()) {
|
|
90
|
+
const fileText = readRelativeTextFile(baseDir, config.splashFile);
|
|
91
|
+
if (fileText != null) return fileText;
|
|
92
|
+
}
|
|
93
|
+
return fallback;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getDefaultSplash() {
|
|
97
|
+
return readOptionalTextFile(defaultTuiSplashPath) || "{bold}{cyan-fg}PilotSwarm{/cyan-fg}{/bold}";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function readPluginMetadata(pluginDir) {
|
|
101
|
+
if (!pluginDir) return null;
|
|
102
|
+
const pluginJsonPath = path.join(pluginDir, "plugin.json");
|
|
103
|
+
if (!fileExists(pluginJsonPath)) return null;
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`Failed to parse plugin metadata: ${pluginJsonPath}: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getPluginDirsFromEnv() {
|
|
112
|
+
const envDirs = String(process.env.PLUGIN_DIRS || "")
|
|
113
|
+
.split(",")
|
|
114
|
+
.map((value) => value.trim())
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map((value) => path.resolve(value));
|
|
117
|
+
if (envDirs.length > 0) return envDirs;
|
|
118
|
+
|
|
119
|
+
const cwdPlugin = path.resolve(process.cwd(), "plugins");
|
|
120
|
+
if (fileExists(cwdPlugin)) return [cwdPlugin];
|
|
121
|
+
|
|
122
|
+
const bundledPlugin = path.join(pkgRoot, "plugins");
|
|
123
|
+
if (fileExists(bundledPlugin)) return [bundledPlugin];
|
|
124
|
+
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function resolveTuiBranding(pluginDir) {
|
|
129
|
+
const pluginMeta = readPluginMetadata(pluginDir);
|
|
130
|
+
const tui = pluginMeta?.tui;
|
|
131
|
+
const defaultSplash = getDefaultSplash();
|
|
132
|
+
if (!tui || typeof tui !== "object") {
|
|
133
|
+
return { title: "PilotSwarm", splash: defaultSplash };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const title = firstNonEmptyString(tui.title, "PilotSwarm") || "PilotSwarm";
|
|
137
|
+
const splash = readSplashValue(pluginDir, tui, defaultSplash);
|
|
138
|
+
return { title, splash };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolvePortalConfigBundleFromPluginDirs(pluginDirs = []) {
|
|
142
|
+
const defaultSplash = getDefaultSplash();
|
|
143
|
+
const defaults = {
|
|
144
|
+
branding: {
|
|
145
|
+
title: "PilotSwarm",
|
|
146
|
+
pageTitle: "PilotSwarm",
|
|
147
|
+
splash: defaultSplash,
|
|
148
|
+
logoUrl: null,
|
|
149
|
+
faviconUrl: null,
|
|
150
|
+
},
|
|
151
|
+
ui: {
|
|
152
|
+
loadingMessage: "Preparing your workspace",
|
|
153
|
+
loadingCopy: "Connecting the shared workspace and live session feeds...",
|
|
154
|
+
},
|
|
155
|
+
auth: {
|
|
156
|
+
provider: null,
|
|
157
|
+
providers: {},
|
|
158
|
+
signInTitle: "Sign in to PilotSwarm",
|
|
159
|
+
signInMessage: null,
|
|
160
|
+
signInLabel: "Sign In",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
for (const pluginDir of pluginDirs) {
|
|
165
|
+
const absDir = path.resolve(pluginDir);
|
|
166
|
+
const pluginMeta = readPluginMetadata(absDir);
|
|
167
|
+
if (!pluginMeta) continue;
|
|
168
|
+
|
|
169
|
+
const portal = getObject(pluginMeta?.portal);
|
|
170
|
+
const portalBranding = getObject(portal.branding);
|
|
171
|
+
const portalUi = getObject(portal.ui);
|
|
172
|
+
const portalAuth = getObject(portal.auth);
|
|
173
|
+
const tui = getObject(pluginMeta?.tui);
|
|
174
|
+
|
|
175
|
+
const title = firstNonEmptyString(portalBranding.title, portal.title, tui.title, defaults.branding.title) || defaults.branding.title;
|
|
176
|
+
const pageTitle = firstNonEmptyString(portalBranding.pageTitle, portal.pageTitle, title, defaults.branding.pageTitle) || defaults.branding.pageTitle;
|
|
177
|
+
const splash = readSplashValue(
|
|
178
|
+
absDir,
|
|
179
|
+
portalBranding,
|
|
180
|
+
readSplashValue(absDir, portal, readSplashValue(absDir, tui, defaults.branding.splash)),
|
|
181
|
+
);
|
|
182
|
+
const logoAsset = resolvePortalAsset(absDir, {
|
|
183
|
+
file: firstNonEmptyString(portalBranding.logoFile, portal.logoFile),
|
|
184
|
+
url: firstNonEmptyString(portalBranding.logoUrl, portal.logoUrl),
|
|
185
|
+
});
|
|
186
|
+
const faviconAsset = resolvePortalAsset(absDir, {
|
|
187
|
+
file: firstNonEmptyString(portalBranding.faviconFile, portal.faviconFile, portalBranding.logoFile, portal.logoFile),
|
|
188
|
+
url: firstNonEmptyString(portalBranding.faviconUrl, portal.faviconUrl, portalBranding.logoUrl, portal.logoUrl),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const assetFiles = {};
|
|
192
|
+
const branding = {
|
|
193
|
+
title,
|
|
194
|
+
pageTitle,
|
|
195
|
+
splash,
|
|
196
|
+
logoUrl: logoAsset.publicUrl || null,
|
|
197
|
+
faviconUrl: faviconAsset.publicUrl || null,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (logoAsset.filePath) {
|
|
201
|
+
assetFiles.logo = logoAsset.filePath;
|
|
202
|
+
branding.logoUrl = "/api/portal-assets/logo";
|
|
203
|
+
}
|
|
204
|
+
if (faviconAsset.filePath) {
|
|
205
|
+
assetFiles.favicon = faviconAsset.filePath;
|
|
206
|
+
branding.faviconUrl = "/api/portal-assets/favicon";
|
|
207
|
+
}
|
|
208
|
+
if (!branding.faviconUrl && branding.logoUrl) {
|
|
209
|
+
branding.faviconUrl = branding.logoUrl;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
portalConfig: {
|
|
214
|
+
branding,
|
|
215
|
+
ui: {
|
|
216
|
+
loadingMessage: firstNonEmptyString(portalUi.loadingMessage, portal.loadingMessage, defaults.ui.loadingMessage) || defaults.ui.loadingMessage,
|
|
217
|
+
loadingCopy: firstNonEmptyString(portalUi.loadingCopy, portal.loadingCopy, defaults.ui.loadingCopy) || defaults.ui.loadingCopy,
|
|
218
|
+
},
|
|
219
|
+
auth: {
|
|
220
|
+
provider: firstNonEmptyString(portalAuth.provider, portal.provider),
|
|
221
|
+
providers: getObject(portalAuth.providers),
|
|
222
|
+
signInTitle: firstNonEmptyString(portalAuth.signInTitle, portal.signInTitle, `Sign in to ${title}`) || `Sign in to ${title}`,
|
|
223
|
+
signInMessage: firstNonEmptyString(portalAuth.signInMessage, portal.signInMessage, defaults.auth.signInMessage),
|
|
224
|
+
signInLabel: firstNonEmptyString(portalAuth.signInLabel, defaults.auth.signInLabel) || defaults.auth.signInLabel,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
assetFiles,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
portalConfig: defaults,
|
|
233
|
+
assetFiles: {},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function resolvePortalConfigFromPluginDirs(pluginDirs = []) {
|
|
238
|
+
return resolvePortalConfigBundleFromPluginDirs(pluginDirs).portalConfig;
|
|
239
|
+
}
|