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/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 = "white";
8
- const SELECTION_FOREGROUND = "black";
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 || undefined,
234
- backgroundColor: run.backgroundColor || undefined,
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 === "gray",
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 || undefined },
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, { color: scrollIndicator ? "gray" : undefined, dimColor: Boolean(scrollIndicator) }, scrollChar),
350
- React.createElement(Text, { color: borderColor }, ""));
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 safeTitleRuns = trimRuns(normalizeTitleRuns(title, color), Math.max(1, safeWidth - 6));
356
- const fill = Math.max(0, safeWidth - titleRunLength(safeTitleRuns) - 5);
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
- React.createElement(Text, { color }, ` ${"─".repeat(fill)}╮`));
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 || undefined,
531
- backgroundColor: line.backgroundColor || undefined,
576
+ color: resolveColorToken(line.color),
577
+ backgroundColor: resolveColorToken(line.backgroundColor),
532
578
  bold: Boolean(line.bold),
533
- dimColor: line.color === "gray",
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 || undefined,
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: "black",
721
- backgroundColor: "green",
768
+ color: resolveColorToken("promptCursorForeground"),
769
+ backgroundColor: resolveColorToken("promptCursorBackground"),
722
770
  dimColor,
723
771
  }, cursorChar)
724
- : React.createElement(Text, { color: "green" }, "█")
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: true,
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: true }, right || ""));
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
+ }
package/src/portal.js ADDED
@@ -0,0 +1,7 @@
1
+ export { NodeSdkTransport } from "./node-sdk-transport.js";
2
+ export {
3
+ getPluginDirsFromEnv,
4
+ readPluginMetadata,
5
+ resolvePortalConfigBundleFromPluginDirs,
6
+ resolvePortalConfigFromPluginDirs,
7
+ } from "./plugin-config.js";