pilotswarm-cli 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +3 -0
  2. package/bin/tui.js +8 -2
  3. package/node_modules/pilotswarm-ui-core/README.md +6 -0
  4. package/node_modules/pilotswarm-ui-core/package.json +32 -0
  5. package/node_modules/pilotswarm-ui-core/src/commands.js +72 -0
  6. package/node_modules/pilotswarm-ui-core/src/context-usage.js +212 -0
  7. package/node_modules/pilotswarm-ui-core/src/controller.js +3676 -0
  8. package/node_modules/pilotswarm-ui-core/src/formatting.js +872 -0
  9. package/node_modules/pilotswarm-ui-core/src/history.js +589 -0
  10. package/node_modules/pilotswarm-ui-core/src/index.js +13 -0
  11. package/node_modules/pilotswarm-ui-core/src/layout.js +196 -0
  12. package/node_modules/pilotswarm-ui-core/src/reducer.js +1030 -0
  13. package/node_modules/pilotswarm-ui-core/src/selectors.js +2921 -0
  14. package/node_modules/pilotswarm-ui-core/src/session-tree.js +109 -0
  15. package/node_modules/pilotswarm-ui-core/src/state.js +80 -0
  16. package/node_modules/pilotswarm-ui-core/src/store.js +23 -0
  17. package/node_modules/pilotswarm-ui-core/src/system-titles.js +24 -0
  18. package/node_modules/pilotswarm-ui-core/src/themes/catppuccin-mocha.js +56 -0
  19. package/node_modules/pilotswarm-ui-core/src/themes/cobalt2.js +56 -0
  20. package/node_modules/pilotswarm-ui-core/src/themes/dark-high-contrast.js +56 -0
  21. package/node_modules/pilotswarm-ui-core/src/themes/dracula.js +56 -0
  22. package/node_modules/pilotswarm-ui-core/src/themes/github-dark.js +56 -0
  23. package/node_modules/pilotswarm-ui-core/src/themes/gruvbox-dark.js +56 -0
  24. package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-matrix.js +56 -0
  25. package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-orion-prime.js +56 -0
  26. package/node_modules/pilotswarm-ui-core/src/themes/helpers.js +77 -0
  27. package/node_modules/pilotswarm-ui-core/src/themes/index.js +44 -0
  28. package/node_modules/pilotswarm-ui-core/src/themes/noctis-obscuro.js +56 -0
  29. package/node_modules/pilotswarm-ui-core/src/themes/noctis-viola.js +56 -0
  30. package/node_modules/pilotswarm-ui-core/src/themes/noctis.js +56 -0
  31. package/node_modules/pilotswarm-ui-core/src/themes/nord.js +56 -0
  32. package/node_modules/pilotswarm-ui-core/src/themes/solarized-dark.js +56 -0
  33. package/node_modules/pilotswarm-ui-core/src/themes/tokyo-night.js +56 -0
  34. package/node_modules/pilotswarm-ui-react/README.md +5 -0
  35. package/node_modules/pilotswarm-ui-react/package.json +36 -0
  36. package/node_modules/pilotswarm-ui-react/src/components.js +1413 -0
  37. package/node_modules/pilotswarm-ui-react/src/index.js +4 -0
  38. package/node_modules/pilotswarm-ui-react/src/platform.js +15 -0
  39. package/node_modules/pilotswarm-ui-react/src/use-controller-state.js +38 -0
  40. package/node_modules/pilotswarm-ui-react/src/web-app.js +2759 -0
  41. package/package.json +9 -3
  42. package/src/app.js +44 -0
  43. package/src/bootstrap-env.js +1 -37
  44. package/src/node-sdk-transport.js +555 -62
  45. package/src/platform.js +43 -11
  46. package/src/plugin-config.js +239 -0
  47. package/src/portal.js +7 -0
  48. package/src/sync-workspace-ui.js +53 -0
package/src/platform.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { Box, Text } from "ink";
4
- import { DEFAULT_THEME_ID, getTheme, 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
7
  const SELECTION_BACKGROUND = "selectionBackground";
@@ -38,8 +38,12 @@ function resolveColorToken(color) {
38
38
  return theme?.tui?.[color] || color;
39
39
  }
40
40
 
41
+ export function shouldDimGrayTextForTheme(theme = getCurrentTheme()) {
42
+ return !isThemeLight(theme);
43
+ }
44
+
41
45
  function isDimColorToken(color) {
42
- return color === "gray";
46
+ return color === "gray" && shouldDimGrayTextForTheme();
43
47
  }
44
48
 
45
49
  function trimText(value, width) {
@@ -361,19 +365,46 @@ function renderPanelRow(line, rowKey, contentWidth, borderColor, scrollIndicator
361
365
  : selectedRuns.length > 0
362
366
  ? renderInlineRuns(selectedRuns, `inline:${rowKey}`)
363
367
  : React.createElement(Text, null, "")),
364
- React.createElement(Text, { color: scrollIndicator ? resolveColorToken("gray") : undefined, dimColor: Boolean(scrollIndicator) }, scrollChar),
368
+ React.createElement(Text, {
369
+ color: scrollIndicator ? resolveColorToken("gray") : undefined,
370
+ dimColor: Boolean(scrollIndicator) && shouldDimGrayTextForTheme(),
371
+ }, scrollChar),
365
372
  React.createElement(Text, { color: resolveColorToken(borderColor) }, "│"));
366
373
  }
367
374
 
368
- function renderBorderTop(title, color, width) {
375
+ function renderBorderTop(title, color, width, titleRight = null) {
369
376
  const safeWidth = Math.max(8, Number(width) || 40);
370
- const safeTitleRuns = trimRuns(normalizeTitleRuns(title, color), Math.max(1, safeWidth - 6));
371
- 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);
372
397
 
373
398
  return React.createElement(Box, null,
374
399
  React.createElement(Text, { color: resolveColorToken(color) }, "╭─ "),
375
400
  renderInlineRuns(safeTitleRuns, "title"),
376
- React.createElement(Text, { color: resolveColorToken(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)}╮`));
377
408
  }
378
409
 
379
410
  function renderBorderBottom(color, width) {
@@ -576,11 +607,12 @@ function Header({ title, subtitle }) {
576
607
  justifyContent: "space-between",
577
608
  },
578
609
  React.createElement(Text, { bold: true, color: resolveColorToken("cyan") }, title),
579
- React.createElement(Text, { color: resolveColorToken("gray"), dimColor: true }, subtitle || ""));
610
+ React.createElement(Text, { color: resolveColorToken("gray"), dimColor: shouldDimGrayTextForTheme() }, subtitle || ""));
580
611
  }
581
612
 
582
613
  function Panel({
583
614
  title,
615
+ titleRight,
584
616
  color = "white",
585
617
  focused = false,
586
618
  width,
@@ -649,7 +681,7 @@ function Panel({
649
681
  flexGrow,
650
682
  flexBasis,
651
683
  },
652
- renderBorderTop(title, borderColor, safeWidth),
684
+ renderBorderTop(title, borderColor, safeWidth, titleRight),
653
685
  lines
654
686
  ? React.createElement(Box, { flexDirection: "column", flexGrow: 1, backgroundColor: fillColor || undefined },
655
687
  [
@@ -773,7 +805,7 @@ function Input({ label, value, focused, placeholder, rows = 1, cursorIndex = 0 }
773
805
  ? [
774
806
  renderPromptRow(placeholder || "Type a message and press Enter", focused ? 0 : null, {
775
807
  color: resolveColorToken("gray"),
776
- dimColor: true,
808
+ dimColor: shouldDimGrayTextForTheme(),
777
809
  showCursor: Boolean(focused),
778
810
  keyPrefix: "prompt-line:0",
779
811
  prefix: labelPrefix,
@@ -800,7 +832,7 @@ function StatusLine({ left, right }) {
800
832
  justifyContent: "space-between",
801
833
  },
802
834
  React.createElement(Text, { color: resolveColorToken("white") }, left || ""),
803
- React.createElement(Text, { color: resolveColorToken("gray"), dimColor: true }, right || ""));
835
+ React.createElement(Text, { color: resolveColorToken("gray"), dimColor: shouldDimGrayTextForTheme() }, right || ""));
804
836
  }
805
837
 
806
838
  function Overlay({ children }) {
@@ -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";
@@ -0,0 +1,53 @@
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 BUNDLED_UI_PACKAGES = ["pilotswarm-ui-core", "pilotswarm-ui-react"];
7
+
8
+ function copyTree(sourceDir, targetDir) {
9
+ fs.mkdirSync(targetDir, { recursive: true });
10
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
11
+ const sourcePath = path.join(sourceDir, entry.name);
12
+ const targetPath = path.join(targetDir, entry.name);
13
+ if (entry.isDirectory()) {
14
+ copyTree(sourcePath, targetPath);
15
+ continue;
16
+ }
17
+ fs.copyFileSync(sourcePath, targetPath);
18
+ }
19
+ }
20
+
21
+ export function syncBundledWorkspaceUiPackages({ cliPackageDir = path.resolve(__dirname, "..") } = {}) {
22
+ const packagesDir = path.resolve(cliPackageDir, "..");
23
+ const syncedPackages = [];
24
+
25
+ for (const packageName of BUNDLED_UI_PACKAGES) {
26
+ const workspaceDir = path.join(packagesDir, packageName.replace("pilotswarm-", ""));
27
+ const sourcePackageJson = path.join(workspaceDir, "package.json");
28
+ const sourceReadme = path.join(workspaceDir, "README.md");
29
+ const sourceSrcDir = path.join(workspaceDir, "src");
30
+ if (!fs.existsSync(sourcePackageJson) || !fs.existsSync(sourceSrcDir)) {
31
+ continue;
32
+ }
33
+
34
+ const targetDir = path.join(cliPackageDir, "node_modules", packageName);
35
+ fs.rmSync(targetDir, { recursive: true, force: true });
36
+ fs.mkdirSync(targetDir, { recursive: true });
37
+ fs.copyFileSync(sourcePackageJson, path.join(targetDir, "package.json"));
38
+ if (fs.existsSync(sourceReadme)) {
39
+ fs.copyFileSync(sourceReadme, path.join(targetDir, "README.md"));
40
+ }
41
+ copyTree(sourceSrcDir, path.join(targetDir, "src"));
42
+ syncedPackages.push(packageName);
43
+ }
44
+
45
+ return syncedPackages;
46
+ }
47
+
48
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
49
+ const syncedPackages = syncBundledWorkspaceUiPackages();
50
+ if (syncedPackages.length > 0) {
51
+ console.log(`[sync-workspace-ui] synced ${syncedPackages.join(", ")}`);
52
+ }
53
+ }