@skyvexsoftware/stratos-sdk 0.1.5 → 0.1.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/README.md CHANGED
@@ -16,169 +16,177 @@ Scaffold a new plugin project:
16
16
  pnpx create-stratos-plugin
17
17
  ```
18
18
 
19
- Or manually create a project with `plugin.json`, `vite.config.ts`, and `src/ui/index.tsx`. See the [plugin developer guide](https://skyvexsoftware.com/docs/plugins) for details.
19
+ This generates a ready-to-develop plugin with Tailwind CSS, TypeScript, and Vite preconfigured.
20
20
 
21
- ## Available APIs
21
+ ### Development
22
22
 
23
- ### Plugin Contract Types
23
+ 1. Open the Stratos app with `--dev` flag
24
+ 2. In your plugin directory, run:
24
25
 
25
- Type definitions for the plugin system:
26
-
27
- ```ts
28
- import type {
29
- PluginManifest,
30
- PluginContext,
31
- PluginUIContext,
32
- PluginBackgroundModule,
33
- PluginUIModule,
34
- } from "@skyvexsoftware/stratos-sdk";
26
+ ```bash
27
+ pnpm dev
35
28
  ```
36
29
 
37
- ### Vite Plugin Config
30
+ Your plugin auto-connects to the running Stratos app and appears in the sidebar. Code changes auto-reload.
38
31
 
39
- The SDK provides a shared Vite configuration for building plugins:
32
+ ### Project Structure
40
33
 
41
- ```ts
42
- import { createPluginConfig } from "@skyvexsoftware/stratos-sdk/vite";
43
-
44
- export default createPluginConfig({
45
- pluginDir: import.meta.dirname,
46
- ui: { entry: "src/ui/index.tsx" },
47
- background: { entry: "src/background/index.ts" }, // optional
48
- });
34
+ ```
35
+ my-plugin/
36
+ ├── plugin.json # Plugin manifest (name, version, author, settings)
37
+ ├── package.json
38
+ ├── vite.config.ts # Uses createPluginConfig from SDK
39
+ ├── tsconfig.json
40
+ ├── src/
41
+ │ ├── ui/
42
+ │ │ ├── index.tsx # UI entry (default export: React component)
43
+ │ │ └── global.css # Tailwind CSS
44
+ │ └── background/ # Optional: main process module
45
+ │ └── index.ts # Exports onStart(ctx) and onStop()
46
+ └── assets/
47
+ ├── icon-light.svg # Sidebar icon (dark theme)
48
+ └── icon-dark.svg # Sidebar icon (light theme)
49
49
  ```
50
50
 
51
- This handles UI bundling with externals, background module builds, asset copying, dev server auto-connect, and HMR.
51
+ ### Building
52
+
53
+ ```bash
54
+ pnpm build # Build for distribution
55
+ ```
52
56
 
53
- ### Helper Functions
57
+ Produces `dist/` with bundled UI, background module (if any), manifest, and assets. Zip and upload to the Skyvex website.
54
58
 
55
- #### `createPlugin(module)`
59
+ ## Vite Config
56
60
 
57
- Validates and returns a typed plugin background module:
61
+ The SDK provides `createPluginConfig` which handles bundling, externals, dev server auto-connect, and asset copying:
58
62
 
59
63
  ```ts
60
- import { createPlugin } from "@skyvexsoftware/stratos-sdk";
64
+ import { createPluginConfig } from "@skyvexsoftware/stratos-sdk/vite";
65
+ import tailwindcss from "@tailwindcss/vite";
61
66
 
62
- export default createPlugin({
63
- async onStart(ctx) {
64
- ctx.logger.info("MyPlugin", "Starting...");
65
- },
66
- async onStop() {
67
- // cleanup
67
+ export default createPluginConfig({
68
+ ui: { entry: "src/ui/index.tsx" },
69
+ background: { entry: "src/background/index.ts" }, // optional
70
+ vite: {
71
+ plugins: [tailwindcss()],
68
72
  },
69
73
  });
70
74
  ```
71
75
 
72
- #### `PluginRouter` component
73
-
74
- Optional lightweight router for plugins that need internal multi-page navigation:
76
+ The `vite` option accepts any Vite config to merge in (plugins, resolve, css, etc.).
75
77
 
76
- ```tsx
77
- import { PluginRouter } from "@skyvexsoftware/stratos-sdk";
78
- import TrackingPage from "./pages/TrackingPage";
79
- import HistoryPage from "./pages/HistoryPage";
80
- import HistoryDetailPage from "./pages/HistoryDetailPage";
78
+ ## Plugin Manifest
81
79
 
82
- const routes = [
83
- { path: "tracking", component: TrackingPage },
84
- { path: "history", component: HistoryPage },
85
- { path: "history/:id", component: HistoryDetailPage },
86
- ];
80
+ `plugin.json` defines your plugin's metadata:
87
81
 
88
- export default function MyPluginRoot() {
89
- return <PluginRouter routes={routes} defaultPath="tracking" />;
82
+ ```json
83
+ {
84
+ "$schema": "https://cdn.skyvexsoftware.com/schemas/stratos-plugin.json",
85
+ "id": "my-plugin",
86
+ "type": "airline",
87
+ "name": "My Plugin",
88
+ "version": "0.1.0",
89
+ "description": "A Stratos plugin",
90
+ "author": { "id": "my-org", "name": "My Organization" },
91
+ "icon_light": "icon-light.svg",
92
+ "icon_dark": "icon-dark.svg"
90
93
  }
91
94
  ```
92
95
 
93
- For single-page plugins, just export the component directly:
96
+ - `type`: `"airline"` (scoped to a virtual airline) or `"user"` (user-installed)
97
+ - `availableSettings`: Optional array of setting definitions (boolean, text, number, list, etc.)
94
98
 
95
- ```tsx
96
- import HomePage from "./HomePage";
97
- export default HomePage;
98
- ```
99
+ ## API Reference
99
100
 
100
- #### `usePluginRoute()`
101
+ ### UI Context
101
102
 
102
- Returns the current sub-path within the plugin (the URL portion after `/plugins/{pluginId}/`):
103
+ Access shell services from plugin components:
103
104
 
104
105
  ```tsx
105
- import { usePluginRoute } from "@skyvexsoftware/stratos-sdk";
106
+ import { usePluginContext } from "@skyvexsoftware/stratos-sdk";
106
107
 
107
108
  function MyComponent() {
108
- const { subPath } = usePluginRoute();
109
- // e.g. "history/abc123"
110
- }
111
- ```
112
-
113
- #### `usePluginParams()`
114
-
115
- Access route params matched by `PluginRouter` (e.g. `history/:id`):
116
-
117
- ```tsx
118
- import { usePluginParams } from "@skyvexsoftware/stratos-sdk";
119
-
120
- function DetailPage() {
121
- const { id } = usePluginParams<{ id: string }>();
122
- // id === "abc123" when URL is /plugins/my-plugin/history/abc123
109
+ const {
110
+ pluginId,
111
+ auth, // { isAuthenticated, token, user }
112
+ airline, // { id, name, icao, logo_light, logo_dark }
113
+ config, // { get(key, defaultValue) }
114
+ navigation, // { navigateTo, navigateToPlugin, navigateToShell }
115
+ toast, // { success, error, info, warning }
116
+ logger, // { info, warn, error, debug }
117
+ } = usePluginContext();
123
118
  }
124
119
  ```
125
120
 
126
- ### React Hooks
127
-
128
- Access shell services from plugin components. All hooks must be used within a `PluginShellProvider` (provided by the shell at runtime).
121
+ Individual hooks are also available:
129
122
 
130
123
  | Hook | Returns | Description |
131
124
  | ---------------------- | -------------------- | ------------------------------ |
125
+ | `usePluginContext()` | Full PluginUIContext | All services combined |
132
126
  | `useShellAuth()` | Auth state | Authentication state and token |
133
127
  | `useShellConfig()` | Config store | Scoped configuration access |
134
128
  | `useShellNavigation()` | Navigation helpers | Route navigation utilities |
135
129
  | `useShellToast()` | Toast API | Toast/notification functions |
136
130
  | `usePluginLogger()` | Logger | Scoped renderer-side logger |
137
- | `usePluginContext()` | Full PluginUIContext | All of the above combined |
138
131
 
139
- ```tsx
140
- import { useShellAuth, useShellToast } from "@skyvexsoftware/stratos-sdk";
132
+ ### Background Module
141
133
 
142
- function MyComponent() {
143
- const { isAuthenticated } = useShellAuth();
144
- const toast = useShellToast();
145
-
146
- return (
147
- <button onClick={() => toast.success("Hello!")}>
148
- {isAuthenticated ? "Logged in" : "Not authenticated"}
149
- </button>
150
- );
151
- }
134
+ Optional main-process code with access to IPC, Express routes, SQLite, and more:
135
+
136
+ ```ts
137
+ import { createPlugin } from "@skyvexsoftware/stratos-sdk";
138
+
139
+ export default createPlugin({
140
+ async onStart(ctx) {
141
+ // ctx.logger — scoped logger
142
+ // ctx.config — per-plugin config store
143
+ // ctx.ipc — IPC handler registration
144
+ // ctx.auth — read-only auth token access
145
+ // ctx.server — Express router registration
146
+ // ctx.database — SQLite database access
147
+ ctx.logger.info("MyPlugin", "Started");
148
+ },
149
+ async onStop() {
150
+ // cleanup
151
+ },
152
+ });
152
153
  ```
153
154
 
154
- ### UI Primitives
155
+ ### Routing
155
156
 
156
- Pre-styled shadcn/ui components using Tailwind CSS and Radix UI:
157
+ For multi-page plugins, use `PluginRouter`:
157
158
 
158
159
  ```tsx
159
- import { Button, Card, CardContent, Input, Label } from "@skyvexsoftware/stratos-sdk";
160
+ import { PluginRouter } from "@skyvexsoftware/stratos-sdk";
161
+
162
+ const routes = [
163
+ { path: "home", component: HomePage },
164
+ { path: "settings", component: SettingsPage },
165
+ { path: "detail/:id", component: DetailPage },
166
+ ];
167
+
168
+ export default function Plugin() {
169
+ return <PluginRouter routes={routes} defaultPath="home" />;
170
+ }
160
171
  ```
161
172
 
162
- Available components:
173
+ For single-page plugins, just export the component directly.
174
+
175
+ ### UI Components
163
176
 
164
- - **Button** with variants: default, destructive, outline, secondary, ghost, link
165
- - **Card** — CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter
166
- - **Dialog** — DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription
167
- - **Input** text input with validation states
168
- - **Label** — form label
169
- - **Select** — SelectTrigger, SelectContent, SelectItem, SelectGroup, SelectValue
170
- - **Badge** — with variants: default, secondary, destructive, outline
171
- - **Separator** — horizontal or vertical divider
172
- - **Tabs** — TabsList, TabsTrigger, TabsContent
173
- - **Tooltip** — TooltipProvider, TooltipTrigger, TooltipContent
177
+ Pre-styled shadcn/ui components that match the Stratos design system:
178
+
179
+ ```tsx
180
+ import { Button, Card, CardContent, Input, Dialog } from "@skyvexsoftware/stratos-sdk";
181
+ ```
174
182
 
175
- ### Shared Types
183
+ Available: Button, Card, Dialog, Input, Label, Select, Badge, Separator, Tabs, Tooltip, AlertDialog, RadioGroup, Slider, Switch, Textarea.
176
184
 
177
- Common types used across plugins:
185
+ ### Types
178
186
 
179
187
  ```ts
180
188
  import { FlightPhase, SimulatorType } from "@skyvexsoftware/stratos-sdk";
181
- import type { FlightData, ThemeMode } from "@skyvexsoftware/stratos-sdk";
189
+ import type { FlightData, PluginManifest, PluginContext } from "@skyvexsoftware/stratos-sdk";
182
190
  ```
183
191
 
184
192
  ### Utilities
@@ -186,21 +194,10 @@ import type { FlightData, ThemeMode } from "@skyvexsoftware/stratos-sdk";
186
194
  ```ts
187
195
  import { cn } from "@skyvexsoftware/stratos-sdk";
188
196
 
189
- // Tailwind class merging utility
190
- cn("bg-red-500", conditional && "text-white", className);
197
+ // Tailwind class merging
198
+ cn("bg-red-500", isActive && "text-white", className);
191
199
  ```
192
200
 
193
- ## Peer Dependencies
194
-
195
- The SDK expects these to be provided by the Stratos shell at runtime:
196
-
197
- - `react` ^19.0.0
198
- - `@radix-ui/react-*` (dialog, label, select, separator, slot, tabs, tooltip)
199
- - `@tanstack/react-query` ^5.0.0
200
- - `class-variance-authority` ^0.7.0
201
- - `lucide-react` >=0.300.0
202
- - `socket.io-client` ^4.0.0
203
-
204
201
  ## License
205
202
 
206
203
  MIT
@@ -29,13 +29,16 @@ declare const BG_EXTERNALS: string[];
29
29
  */
30
30
  declare function stratosExternals(): Plugin;
31
31
  export type PluginBuildOptions = {
32
- pluginDir: string;
32
+ /** Plugin root directory. Defaults to process.cwd(). */
33
+ pluginDir?: string;
33
34
  ui?: {
34
35
  entry: string;
35
36
  };
36
37
  background?: {
37
38
  entry: string;
38
39
  };
40
+ /** Extra Vite config to merge (plugins, css, etc.) */
41
+ vite?: UserConfig;
39
42
  };
40
43
  /**
41
44
  * Create a Vite config for building a Stratos plugin.
@@ -157,26 +157,20 @@ function copyDirSync(src, dest) {
157
157
  * - "background" → builds the background module (Node.js, ESM)
158
158
  */
159
159
  export function createPluginConfig(options) {
160
- const { pluginDir, ui, background } = options;
160
+ const { pluginDir = process.cwd(), ui, background, vite: extraConfig, } = options;
161
161
  const target = process.env.BUILD_TARGET;
162
162
  if (target === "background" && background) {
163
163
  return createBackgroundConfig(pluginDir, background.entry);
164
164
  }
165
165
  if (ui) {
166
- return createUIConfig(pluginDir, ui.entry);
166
+ return createUIConfig(pluginDir, ui.entry, extraConfig);
167
167
  }
168
168
  throw new Error("No build target. Provide a ui entry or set BUILD_TARGET=background.");
169
169
  }
170
- function createUIConfig(pluginDir, entry) {
170
+ function createUIConfig(pluginDir, entry, extraConfig) {
171
171
  const isProduction = process.env.NODE_ENV === "production";
172
172
  return {
173
173
  root: pluginDir,
174
- plugins: [
175
- serveExternals(),
176
- stratosDevServer({ pluginDir }),
177
- stratosExternals(),
178
- copyPluginAssets(pluginDir),
179
- ],
180
174
  server: {
181
175
  cors: {
182
176
  origin: [
@@ -185,6 +179,14 @@ function createUIConfig(pluginDir, entry) {
185
179
  /^http:\/\/localhost:\d+$/,
186
180
  ],
187
181
  },
182
+ hmr: {
183
+ // When modules are loaded via stratos-dev:// protocol, Vite's HMR
184
+ // client needs to connect back to THIS dev server's WebSocket directly.
185
+ // Without this, it would try to derive the WS URL from the custom
186
+ // protocol scheme, which doesn't work.
187
+ protocol: "ws",
188
+ host: "localhost",
189
+ },
188
190
  },
189
191
  optimizeDeps: {
190
192
  exclude: [
@@ -212,6 +214,15 @@ function createUIConfig(pluginDir, entry) {
212
214
  jsx: "automatic",
213
215
  jsxImportSource: "react",
214
216
  },
217
+ // Merge extra config: plugins are concatenated, other keys shallow-merged
218
+ ...extraConfig,
219
+ plugins: [
220
+ serveExternals(),
221
+ stratosDevServer({ pluginDir }),
222
+ stratosExternals(),
223
+ copyPluginAssets(pluginDir),
224
+ ...(extraConfig?.plugins ?? []),
225
+ ],
215
226
  };
216
227
  }
217
228
  function createBackgroundConfig(pluginDir, entry) {
@@ -19,7 +19,7 @@ export function serveExternals() {
19
19
  return {
20
20
  name: "stratos-serve-externals",
21
21
  apply: "serve",
22
- enforce: "pre",
22
+ enforce: "post",
23
23
  transform(code, id) {
24
24
  // Only process JS/TS source files
25
25
  const ext = id.slice(id.lastIndexOf("."));
@@ -69,6 +69,23 @@ export function serveExternals() {
69
69
  // 6. Side-effect imports: import "pkg"
70
70
  result = result.replace(new RegExp(`import\\s+['"]${escaped}['"]\\s*;?`, "g"), `// (stratos-serve-externals: side-effect import of "${modId}" removed)`);
71
71
  }
72
+ // Second pass: catch Vite-resolved paths like
73
+ // import { jsxDEV } from "/node_modules/react/jsx-dev-runtime.js?v=..."
74
+ // These are injected by esbuild's JSX transform after our source-level pass.
75
+ for (const modId of EXTERNAL_PACKAGES) {
76
+ // Convert package name to a path pattern: "react/jsx-dev-runtime" -> "react/jsx-dev-runtime"
77
+ const pathPattern = modId.replace(/\//g, "/");
78
+ const global = `window.__stratos_modules__[${JSON.stringify(modId)}]`;
79
+ // Match: import { x } from "/node_modules/{pkg}.js?v=..." or "/{pkg}?..."
80
+ const resolvedRe = new RegExp(`import\\s*\\{([^}]+)\\}\\s+from\\s+['"][^'"]*/${escapeRegExp(pathPattern)}(?:\\.js)?(?:\\?[^'"]*)?['"]\\s*;?`, "g");
81
+ result = result.replace(resolvedRe, (_, names) => {
82
+ const destructured = names.replace(/([\w$]+)\s+as\s+([\w$]+)/g, "$1: $2");
83
+ return `const {${destructured}} = ${global};`;
84
+ });
85
+ // Match: import Default from "/node_modules/{pkg}.js?v=..."
86
+ const resolvedDefaultRe = new RegExp(`import\\s+([\\w$]+)\\s+from\\s+['"][^'"]*/${escapeRegExp(pathPattern)}(?:\\.js)?(?:\\?[^'"]*)?['"]\\s*;?`, "g");
87
+ result = result.replace(resolvedDefaultRe, (_, def) => `const ${def} = ${global}.default || ${global};`);
88
+ }
72
89
  if (result === code)
73
90
  return null;
74
91
  return { code: result, map: null };
@@ -20,6 +20,9 @@ import { io as ioClient } from "socket.io-client";
20
20
  */
21
21
  export function stratosDevServer(options) {
22
22
  const { pluginDir } = options;
23
+ // Shared state between configureServer and handleHotUpdate hooks
24
+ let socket = null;
25
+ let pluginId = "unknown";
23
26
  return {
24
27
  name: "stratos-dev-server",
25
28
  apply: "serve",
@@ -28,7 +31,6 @@ export function stratosDevServer(options) {
28
31
  (process.env.STRATOS_PORT
29
32
  ? parseInt(process.env.STRATOS_PORT, 10)
30
33
  : 2066);
31
- let socket = null;
32
34
  let retryTimeout = null;
33
35
  let retryDelay = 3000;
34
36
  const MAX_RETRY_DELAY = 30000;
@@ -41,7 +43,7 @@ export function stratosDevServer(options) {
41
43
  catch {
42
44
  console.error("[stratos-dev-server] Failed to read plugin.json at", manifestPath);
43
45
  }
44
- const pluginId = manifest.id ?? "unknown";
46
+ pluginId = manifest.id ?? "unknown";
45
47
  function clearRetry() {
46
48
  if (retryTimeout) {
47
49
  clearTimeout(retryTimeout);
@@ -183,9 +185,35 @@ export function stratosDevServer(options) {
183
185
  process.once("SIGTERM", cleanup);
184
186
  // Start connecting after the HTTP server is listening
185
187
  server.httpServer?.once("listening", () => {
188
+ // Set the HMR port dynamically so Vite's HMR client connects
189
+ // directly to this dev server's WebSocket (cross-origin is OK for WS).
190
+ const addr = server.httpServer?.address();
191
+ if (addr && typeof addr === "object") {
192
+ server.config.server.hmr =
193
+ typeof server.config.server.hmr === "object"
194
+ ? {
195
+ ...server.config.server.hmr,
196
+ port: addr.port,
197
+ clientPort: addr.port,
198
+ }
199
+ : { port: addr.port, clientPort: addr.port };
200
+ }
186
201
  connect();
187
202
  });
188
203
  },
204
+ // Notify Stratos when UI source files change so it can reload the plugin
205
+ handleHotUpdate({ file }) {
206
+ // Only signal for UI source changes (not background, not config files)
207
+ if (file.includes("/src/") && !file.includes("/src/background/")) {
208
+ if (socket?.connected) {
209
+ console.log(`[stratos-dev-server] UI source changed: ${path.basename(file)}`);
210
+ socket.emit("dev:plugin-ui-updated", {
211
+ pluginId,
212
+ timestamp: Date.now(),
213
+ });
214
+ }
215
+ }
216
+ },
189
217
  };
190
218
  }
191
219
  //# sourceMappingURL=stratos-dev-server.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyvexsoftware/stratos-sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Plugin SDK for Stratos — types, hooks, and UI components",
5
5
  "author": {
6
6
  "name": "Skyvex Software",