skybridge 0.0.0-dev.f78ee95 → 0.0.0-dev.f79f9cd
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 +155 -126
- package/dist/cli/header.js +1 -1
- package/dist/cli/header.js.map +1 -1
- package/dist/cli/tunnel-control-server.d.ts +9 -0
- package/dist/cli/tunnel-control-server.js +31 -0
- package/dist/cli/tunnel-control-server.js.map +1 -0
- package/dist/cli/tunnel-control-server.test.js +39 -0
- package/dist/cli/tunnel-control-server.test.js.map +1 -0
- package/dist/cli/tunnel-handler.d.ts +3 -0
- package/dist/cli/tunnel-handler.js +48 -0
- package/dist/cli/tunnel-handler.js.map +1 -0
- package/dist/cli/tunnel-handler.test.js +105 -0
- package/dist/cli/tunnel-handler.test.js.map +1 -0
- package/dist/cli/tunnel.d.ts +57 -0
- package/dist/cli/tunnel.js +154 -0
- package/dist/cli/tunnel.js.map +1 -0
- package/dist/cli/tunnel.test.js +190 -0
- package/dist/cli/tunnel.test.js.map +1 -0
- package/dist/cli/types.d.ts +5 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/use-messages.d.ts +3 -0
- package/dist/cli/use-messages.js +11 -0
- package/dist/cli/use-messages.js.map +1 -0
- package/dist/cli/use-nodemon.d.ts +2 -7
- package/dist/cli/use-nodemon.js +8 -20
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-tunnel.d.ts +14 -0
- package/dist/cli/use-tunnel.js +131 -0
- package/dist/cli/use-tunnel.js.map +1 -0
- package/dist/cli/use-typescript-check.d.ts +1 -0
- package/dist/cli/use-typescript-check.js +41 -6
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/build.js +63 -7
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +40 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.js +7 -10
- package/dist/commands/start.js.map +1 -1
- package/dist/server/asset-base-url-transform-plugin.d.ts +5 -6
- package/dist/server/asset-base-url-transform-plugin.js +9 -10
- package/dist/server/asset-base-url-transform-plugin.js.map +1 -1
- package/dist/server/asset-base-url-transform-plugin.test.js +41 -13
- package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -1
- package/dist/server/content-helpers.d.ts +27 -0
- package/dist/server/content-helpers.js +46 -0
- package/dist/server/content-helpers.js.map +1 -0
- package/dist/server/content-helpers.test.d.ts +1 -0
- package/dist/server/content-helpers.test.js +70 -0
- package/dist/server/content-helpers.test.js.map +1 -0
- package/dist/server/express.d.ts +6 -2
- package/dist/server/express.js +53 -22
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.test.js +170 -2
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/inferUtilityTypes.d.ts +6 -6
- package/dist/server/metric.d.ts +14 -0
- package/dist/server/metric.js +62 -0
- package/dist/server/metric.js.map +1 -0
- package/dist/server/middleware.test.js +12 -9
- package/dist/server/middleware.test.js.map +1 -1
- package/dist/server/server.d.ts +112 -74
- package/dist/server/server.js +251 -66
- package/dist/server/server.js.map +1 -1
- package/dist/server/templateHelper.d.ts +5 -7
- package/dist/server/templateHelper.js +3 -22
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.d.ts +4 -0
- package/dist/server/templates.generated.js +47 -0
- package/dist/server/templates.generated.js.map +1 -0
- package/dist/server/tunnel-proxy-router.d.ts +7 -0
- package/dist/server/tunnel-proxy-router.js +110 -0
- package/dist/server/tunnel-proxy-router.js.map +1 -0
- package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
- package/dist/server/tunnel-proxy-router.test.js +229 -0
- package/dist/server/tunnel-proxy-router.test.js.map +1 -0
- package/dist/server/viewsDevServer.d.ts +14 -0
- package/dist/server/viewsDevServer.js +45 -0
- package/dist/server/viewsDevServer.js.map +1 -0
- package/dist/test/utils.d.ts +13 -21
- package/dist/test/utils.js +42 -37
- package/dist/test/utils.js.map +1 -1
- package/dist/test/view.test.d.ts +1 -0
- package/dist/test/view.test.js +523 -0
- package/dist/test/view.test.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/web/bridges/apps-sdk/adaptor.d.ts +5 -3
- package/dist/web/bridges/apps-sdk/adaptor.js +32 -14
- package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -1
- package/dist/web/bridges/apps-sdk/types.d.ts +10 -5
- package/dist/web/bridges/apps-sdk/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/adaptor.d.ts +15 -5
- package/dist/web/bridges/mcp-app/adaptor.js +106 -22
- package/dist/web/bridges/mcp-app/adaptor.js.map +1 -1
- package/dist/web/bridges/types.d.ts +15 -8
- package/dist/web/components/modal-provider.js +2 -2
- package/dist/web/components/modal-provider.js.map +1 -1
- package/dist/web/create-store.js +17 -3
- package/dist/web/create-store.js.map +1 -1
- package/dist/web/create-store.test.js +14 -16
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.d.ts +1 -1
- package/dist/web/data-llm.js +3 -3
- package/dist/web/data-llm.js.map +1 -1
- package/dist/web/data-llm.test.js +22 -22
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.d.ts +20 -18
- package/dist/web/generate-helpers.js +20 -18
- package/dist/web/generate-helpers.js.map +1 -1
- package/dist/web/generate-helpers.test-d.js +26 -26
- package/dist/web/generate-helpers.test-d.js.map +1 -1
- package/dist/web/helpers/state.d.ts +2 -2
- package/dist/web/helpers/state.js +11 -11
- package/dist/web/helpers/state.js.map +1 -1
- package/dist/web/helpers/state.test.js +9 -9
- package/dist/web/helpers/state.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +1 -1
- package/dist/web/hooks/index.js +1 -1
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/use-files.d.ts +2 -1
- package/dist/web/hooks/use-files.js +1 -0
- package/dist/web/hooks/use-files.js.map +1 -1
- package/dist/web/hooks/use-files.test.js +22 -2
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.d.ts +1 -1
- package/dist/web/hooks/use-request-modal.js +4 -4
- package/dist/web/hooks/use-request-modal.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +5 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-user.js +18 -2
- package/dist/web/hooks/use-user.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +28 -0
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.d.ts +4 -0
- package/dist/web/hooks/use-view-state.js +32 -0
- package/dist/web/hooks/use-view-state.js.map +1 -0
- package/dist/web/hooks/use-view-state.test.d.ts +1 -0
- package/dist/web/hooks/use-view-state.test.js +177 -0
- package/dist/web/hooks/use-view-state.test.js.map +1 -0
- package/dist/web/index.d.ts +1 -2
- package/dist/web/index.js +1 -2
- package/dist/web/index.js.map +1 -1
- package/dist/web/mount-view.d.ts +1 -0
- package/dist/web/{mount-widget.js → mount-view.js} +2 -2
- package/dist/web/mount-view.js.map +1 -0
- package/dist/web/plugin/plugin.d.ts +4 -1
- package/dist/web/plugin/plugin.js +134 -25
- package/dist/web/plugin/plugin.js.map +1 -1
- package/dist/web/plugin/scan-views.d.ts +16 -0
- package/dist/web/plugin/scan-views.js +88 -0
- package/dist/web/plugin/scan-views.js.map +1 -0
- package/dist/web/plugin/scan-views.test.d.ts +1 -0
- package/dist/web/plugin/scan-views.test.js +99 -0
- package/dist/web/plugin/scan-views.test.js.map +1 -0
- package/dist/web/plugin/transform-data-llm.js +1 -1
- package/dist/web/plugin/transform-data-llm.js.map +1 -1
- package/dist/web/plugin/validate-view.d.ts +1 -0
- package/dist/web/plugin/validate-view.js +9 -0
- package/dist/web/plugin/validate-view.js.map +1 -0
- package/dist/web/plugin/validate-view.test.d.ts +1 -0
- package/dist/web/plugin/validate-view.test.js +24 -0
- package/dist/web/plugin/validate-view.test.js.map +1 -0
- package/package.json +23 -16
- package/tsconfig.base.json +2 -0
- package/dist/server/templates/development.hbs +0 -12
- package/dist/server/templates/production.hbs +0 -6
- package/dist/server/widgetsDevServer.d.ts +0 -13
- package/dist/server/widgetsDevServer.js +0 -57
- package/dist/server/widgetsDevServer.js.map +0 -1
- package/dist/test/widget.test.js +0 -263
- package/dist/test/widget.test.js.map +0 -1
- package/dist/web/hooks/use-widget-state.d.ts +0 -4
- package/dist/web/hooks/use-widget-state.js +0 -32
- package/dist/web/hooks/use-widget-state.js.map +0 -1
- package/dist/web/hooks/use-widget-state.test.js +0 -64
- package/dist/web/hooks/use-widget-state.test.js.map +0 -1
- package/dist/web/mount-widget.d.ts +0 -1
- package/dist/web/mount-widget.js.map +0 -1
- package/dist/web/plugin/validate-widget.d.ts +0 -5
- package/dist/web/plugin/validate-widget.js +0 -27
- package/dist/web/plugin/validate-widget.js.map +0 -1
- package/dist/web/plugin/validate-widget.test.js +0 -42
- package/dist/web/plugin/validate-widget.test.js.map +0 -1
- /package/dist/{test/widget.test.d.ts → cli/tunnel-control-server.test.d.ts} +0 -0
- /package/dist/{web/hooks/use-widget-state.test.d.ts → cli/tunnel-handler.test.d.ts} +0 -0
- /package/dist/{web/plugin/validate-widget.test.d.ts → cli/tunnel.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,149 +1,178 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
<img alt="Skybridge" src="
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://skybridge.tech">
|
|
3
|
+
<img alt="Skybridge" src="docs/images/skybridge-readme-banner-new.png" width="100%" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>The full-stack React framework for MCP Apps and MCP Servers.</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://docs.skybridge.tech">Documentation</a> ·
|
|
13
|
+
<a href="https://docs.skybridge.tech/quickstart/create-new-app">Quickstart</a> ·
|
|
14
|
+
<a href="https://github.com/alpic-ai/skybridge/tree/main/examples">Examples</a> ·
|
|
15
|
+
<a href="https://docs.skybridge.tech/showcase">Showcase</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img alt="Skybridge app preview" src="docs/images/skybridge-readme-preview.png" width="520" />
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<a href="https://www.npmjs.com/package/skybridge">
|
|
24
|
+
<picture>
|
|
25
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/npm/v/skybridge?color=77F5EE&labelColor=161B22&style=for-the-badge">
|
|
26
|
+
<img alt="npm version" src="https://img.shields.io/npm/v/skybridge?color=E3FAF7&labelColor=F6F8FA&style=for-the-badge">
|
|
27
|
+
</picture>
|
|
28
|
+
</a>
|
|
29
|
+
<a href="https://www.npmjs.com/package/skybridge">
|
|
30
|
+
<picture>
|
|
31
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/npm/dm/skybridge?color=D7FFC8&labelColor=161B22&style=for-the-badge">
|
|
32
|
+
<img alt="npm downloads" src="https://img.shields.io/npm/dm/skybridge?color=E8FBD9&labelColor=F6F8FA&style=for-the-badge">
|
|
33
|
+
</picture>
|
|
34
|
+
</a>
|
|
35
|
+
<a href="https://discord.com/invite/gNAazGueab">
|
|
36
|
+
<picture>
|
|
37
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/Discord-community-77F5EE?style=for-the-badge&logo=discord&logoColor=77F5EE&labelColor=161B22">
|
|
38
|
+
<img alt="Discord community" src="https://img.shields.io/badge/Discord-community-E3FAF7?style=for-the-badge&logo=discord&logoColor=5865F2&labelColor=F6F8FA">
|
|
39
|
+
</picture>
|
|
40
|
+
</a>
|
|
41
|
+
<a href="https://github.com/alpic-ai/skybridge/blob/main/LICENSE">
|
|
42
|
+
<picture>
|
|
43
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/github/license/alpic-ai/skybridge?color=D7FFC8&labelColor=161B22&style=for-the-badge">
|
|
44
|
+
<img alt="License: MIT" src="https://img.shields.io/github/license/alpic-ai/skybridge?color=E8FBD9&labelColor=F6F8FA&style=for-the-badge">
|
|
45
|
+
</picture>
|
|
46
|
+
</a>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
## Why Skybridge?
|
|
50
|
+
|
|
51
|
+
MCP Apps is an extension of the [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) adding **rich, interactive UI views** to MCP servers. Building conversational Apps requires new UX design concepts, developer tooling and abstractions to build apps that make interactions between the user, the UI and the model seamless.
|
|
52
|
+
|
|
53
|
+
Skybridge is the answer to all the problems we ran into while building hundreds of production MCP Apps. Features include:
|
|
54
|
+
|
|
55
|
+
- **Delightful dev environment**: It provides a dev server with a local emulator, Hot Module Reload, and a permanent tunnel to connect your local app to Claude and ChatGPT.
|
|
56
|
+
- **Write once, run everywhere**: Skybridge abstracts implementation differences between MCP Clients, so your app runs seamlessly in Claude, ChatGPT, VSCode, and any other compatible MCP Apps client.
|
|
57
|
+
- **Agent-ready**: Powerful Skills, CLI, and programmatic DevTools APIs: it provides everything your coding agent needs to build MCP Apps end-to-end.
|
|
58
|
+
- **Type-safe end-to-end**: tRPC-style inference from MCP server tool definition to React view for type-safety end-to-end from server to frontend.
|
|
59
|
+
- **React-first**: Intuitive React Query-style hooks, with advanced state management.
|
|
60
|
+
- **Examples library**: Get started quickly with production-ready app examples for e-commerce, travel, SaaS, and others.
|
|
61
|
+
|
|
62
|
+
These companies chose Skybridge to deploy their apps on ChatGPT and Claude stores:
|
|
63
|
+
|
|
64
|
+
<p align="center">
|
|
65
|
+
<a href="https://www.datadoghq.com">
|
|
66
|
+
<picture>
|
|
67
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/user-logos/datadog-dark.svg">
|
|
68
|
+
<img src="docs/images/user-logos/datadog-light.svg" alt="Datadog" height="24">
|
|
69
|
+
</picture>
|
|
70
|
+
</a>
|
|
71
|
+
|
|
72
|
+
<a href="https://bitmovin.com">
|
|
73
|
+
<picture>
|
|
74
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/user-logos/bitmovin-dark.svg">
|
|
75
|
+
<img src="docs/images/user-logos/bitmovin-light.svg" alt="Bitmovin" height="22">
|
|
76
|
+
</picture>
|
|
77
|
+
</a>
|
|
78
|
+
|
|
79
|
+
<a href="https://www.evaneos.com">
|
|
80
|
+
<picture>
|
|
81
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/user-logos/evaneos-dark.svg">
|
|
82
|
+
<img src="docs/images/user-logos/evaneos-light.svg" alt="Evaneos" height="18">
|
|
83
|
+
</picture>
|
|
84
|
+
</a>
|
|
85
|
+
|
|
86
|
+
<a href="https://www.touchstream.media">
|
|
87
|
+
<picture>
|
|
88
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/user-logos/touchstream-dark.svg">
|
|
89
|
+
<img src="docs/images/user-logos/touchstream-light.svg" alt="Touchstream" height="24">
|
|
90
|
+
</picture>
|
|
91
|
+
</a>
|
|
92
|
+
|
|
93
|
+
<a href="https://www.cottages.com">
|
|
94
|
+
<picture>
|
|
95
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/user-logos/cottages-dark.svg">
|
|
96
|
+
<img src="docs/images/user-logos/cottages-light.svg" alt="Cottages.com" height="24">
|
|
97
|
+
</picture>
|
|
98
|
+
</a>
|
|
99
|
+
</p>
|
|
100
|
+
|
|
101
|
+
## Get started
|
|
102
|
+
|
|
103
|
+
**For agents**
|
|
104
|
+
|
|
105
|
+
Install our [Skill](https://docs.skybridge.tech/devtools/skills) for MCP Apps and ChatGPT Apps:
|
|
46
106
|
```bash
|
|
47
|
-
|
|
107
|
+
npx skills add alpic-ai/skybridge -s skybridge
|
|
48
108
|
```
|
|
109
|
+
Once installed, if you ask your agent "_what skills do you have?_", it should mention the skybridge skill. Then, you can ask it to:
|
|
49
110
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
npm i skybridge
|
|
54
|
-
yarn add skybridge
|
|
55
|
-
pnpm add skybridge
|
|
56
|
-
bun add skybridge
|
|
57
|
-
deno add skybridge
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
<div align="center">
|
|
61
|
-
|
|
62
|
-
**👉 [Read the Docs](https://docs.skybridge.tech) 👈**
|
|
63
|
-
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<br />
|
|
111
|
+
- _Create a new MCP App_
|
|
112
|
+
- _Migrate my MCP Server to Skybridge framework_
|
|
113
|
+
- _Add a new view to my MCP App_
|
|
67
114
|
|
|
68
|
-
|
|
115
|
+
**For humans**
|
|
69
116
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- **`skybridge/web`** — React hooks that consume your server types. Works with Apps SDK (ChatGPT) and MCP Apps.
|
|
74
|
-
- **Dev Environment** — Vite plugin with HMR, DevTools emulator, and optimized builds.
|
|
75
|
-
|
|
76
|
-
### Server
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { McpServer } from "skybridge/server";
|
|
80
|
-
|
|
81
|
-
server.registerWidget("flights", {}, {
|
|
82
|
-
inputSchema: { destination: z.string() },
|
|
83
|
-
}, async ({ destination }) => {
|
|
84
|
-
const flights = await searchFlights(destination);
|
|
85
|
-
return { structuredContent: { flights } };
|
|
86
|
-
});
|
|
117
|
+
Bootstrap a new project with:
|
|
118
|
+
```bash
|
|
119
|
+
npm create skybridge my-app
|
|
87
120
|
```
|
|
121
|
+
For full install instructions, read the [**Quickstart section**](https://docs.skybridge.tech/quickstart/create-new-app) of our documentation.
|
|
88
122
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
```tsx
|
|
92
|
-
import { useToolInfo } from "skybridge/web";
|
|
123
|
+
## Documentation
|
|
93
124
|
|
|
94
|
-
|
|
95
|
-
const { output } = useToolInfo();
|
|
96
|
-
|
|
97
|
-
return output.structuredContent.flights.map(flight =>
|
|
98
|
-
<FlightCard key={flight.id} flight={flight} />
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
```
|
|
125
|
+
The [Skybridge documentation](https://docs.skybridge.tech) covers the full lifecycle of building MCP Apps:
|
|
102
126
|
|
|
103
|
-
|
|
127
|
+
- [Fundamentals](https://docs.skybridge.tech/fundamentals): understand MCP Apps, ChatGPT Apps, and how Skybridge bridges both runtimes.
|
|
128
|
+
- [Core concepts](https://docs.skybridge.tech/concepts): learn data flow, LLM context sync, type safety, and fast local iteration.
|
|
129
|
+
- [Guides](https://docs.skybridge.tech/guides/fetching-data): build real app behavior with tools, views, state, and model communication.
|
|
130
|
+
- [API Reference](https://docs.skybridge.tech/api-reference): browse server APIs, React hooks, CLI commands, and runtime compatibility.
|
|
104
131
|
|
|
105
|
-
##
|
|
132
|
+
## Deploy
|
|
106
133
|
|
|
107
|
-
|
|
108
|
-
- **Typed Hooks** — Full autocomplete for tools, inputs, outputs.
|
|
109
|
-
- **Widget → Tool Calls** — Trigger server actions from UI.
|
|
110
|
-
- **Dual Surface Sync** — Keep model aware of what users see with `data-llm`.
|
|
111
|
-
- **React Query-style API** — `isPending`, `isError`, callbacks.
|
|
112
|
-
- **Platform Agnostic** — Works with ChatGPT (Apps SDK) and MCP Apps clients (Goose, VSCode, etc.).
|
|
113
|
-
- **MCP Compatible** — Extends the official SDK. Works with any MCP client.
|
|
134
|
+
Deploy Skybridge apps instantly on [Alpic](https://alpic.ai) to get scalable hosting, MCP Analytics, permanent tunnel, MCP auditing and app stores submission help, or self-host on any Node.js-compatible platform.
|
|
114
135
|
|
|
115
|
-
|
|
136
|
+
Read the [deployment guide](https://docs.skybridge.tech/quickstart/deploy) for the full production path.
|
|
116
137
|
|
|
117
|
-
##
|
|
138
|
+
## Example templates
|
|
118
139
|
|
|
119
|
-
Explore
|
|
140
|
+
Explore all our example templates in the [Examples](https://docs.skybridge.tech/examples) section of the documentation.
|
|
120
141
|
|
|
121
|
-
|
|
|
122
|
-
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
142
|
+
| Preview | App | Description | Demo | Code |
|
|
143
|
+
| --- | --- | --- | --- | --- |
|
|
144
|
+
| <img src="docs/images/showcase-capitals.png" alt="Capitals Explorer" width="160" /> | Capitals Explorer | Interactive world map with geolocation, country information, and dynamic capital exploration. | [Try Demo](https://capitals.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/capitals) |
|
|
145
|
+
| <img src="docs/images/showcase-flight-booking.png" alt="Flight Booking" width="160" /> | Flight Booking | Flight search carousel with route details, pricing comparison, and external booking. | [Try Demo](https://flight-booking.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/flight-booking) |
|
|
146
|
+
| <img src="docs/images/showcase-ecommerce.png" alt="Ecommerce Carousel" width="160" /> | Ecommerce Carousel | Product carousel with persistent cart, localization, theme switching, and modal dialogs. | [Try Demo](https://ecommerce.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/ecom-carousel) |
|
|
147
|
+
| <img src="docs/images/showcase-example.png" alt="Everything" width="160" /> | Everything | Comprehensive playground showcasing all Skybridge hooks and features. | [Try Demo](https://everything.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/everything) |
|
|
148
|
+
| <img src="docs/images/showcase-investigation-game.png" alt="Investigation Game" width="160" /> | Investigation Game | Multi-screen mystery game with fullscreen mode and dynamic story progression. | [Try Demo](https://investigation-game.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/investigation-game) |
|
|
149
|
+
| <img src="docs/images/showcase-productivity.png" alt="Productivity" width="160" /> | Productivity | Data visualization dashboard demonstrating Skybridge capabilities for MCP Apps. | [Try Demo](https://productivity.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/productivity) |
|
|
150
|
+
| <img src="docs/images/showcase-times-up.png" alt="Time's Up" width="160" /> | Time's Up | Word-guessing party game where the user gives hints and the AI tries to guess. | [Try Demo](https://times-up.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/times-up) |
|
|
151
|
+
| <img src="docs/images/showcase-manifest-ui.png" alt="Manifest UI" width="160" /> | Manifest UI | Agentic component library example for rich AI-powered experiences. | [Try Demo](https://manifest-ui.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/manifest-ui) |
|
|
152
|
+
| <img src="docs/images/showcase-generative-ui.png" alt="Generative UI" width="160" /> | Generative UI | LLM-generated dynamic UIs with json-render and 36 pre-built shadcn/ui components. | [Try Demo](https://generative-ui.skybridge.tech/try) | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/generative-ui) |
|
|
153
|
+
| <img src="docs/images/showcase-magic-8-ball.png" alt="Magic 8-Ball" width="160" /> | Magic 8-Ball | Default starter app for the core Skybridge loop: input, server logic, and view rendering. | [Try Demo](https://magic-8-ball.skybridge.tech/try) | [View code](https://github.com/alpic-ai/apps-sdk-template) |
|
|
154
|
+
| <img src="docs/images/showcase-clerk.png" alt="Auth Clerk" width="160" /> | Auth — Clerk | Full OAuth authentication with Clerk and personalized coffee shop search. | — | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-clerk) |
|
|
155
|
+
| <img src="docs/images/showcase-workos.png" alt="Auth WorkOS AuthKit" width="160" /> | Auth — WorkOS AuthKit | Full OAuth authentication with WorkOS AuthKit and personalized coffee shop search. | — | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-workos) |
|
|
156
|
+
| <img src="docs/images/showcase-stytch.png" alt="Auth Stytch" width="160" /> | Auth — Stytch | Full OAuth authentication with Stytch and personalized coffee shop search. | — | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-stytch) |
|
|
157
|
+
| <img src="docs/images/showcase-auth0.png" alt="Auth Auth0" width="160" /> | Auth — Auth0 | Full OAuth authentication with Auth0 and personalized coffee shop search. | — | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-auth0) |
|
|
158
|
+
| <img src="docs/images/showcase-lumo.png" alt="Lumo Interactive AI Tutor" width="160" /> | Lumo — Interactive AI Tutor | Adaptive tutor with Mermaid diagrams, mind maps, quizzes, and fill-in-the-blank exercises. | [Try Demo](https://lumo-mcp-app-39519fdd.alpic.live/try) | [View code](https://github.com/connorads/lumo-mcp-app) |
|
|
132
159
|
|
|
133
|
-
|
|
160
|
+
## Community & Contributing
|
|
134
161
|
|
|
135
|
-
|
|
162
|
+
We invite you to contribute and help improve Skybridge.
|
|
136
163
|
|
|
137
|
-
|
|
164
|
+
Here are a few ways you can get involved:
|
|
138
165
|
|
|
139
|
-
[
|
|
140
|
-
[
|
|
141
|
-
[
|
|
166
|
+
- **Reporting bugs**: If you run into a bug or unexpected behavior, please open an issue in [GitHub Issues](https://github.com/alpic-ai/skybridge/issues) with a clear reproduction.
|
|
167
|
+
- **Questions and Suggestions**: Need help building with Skybridge or have ideas to improve the framework, docs, examples, or developer experience? [Open an issue](https://github.com/alpic-ai/skybridge/issues) or share them on our [Discord](https://discord.com/invite/gNAazGueab).
|
|
168
|
+
- **Pull requests**: For code or documentation changes, please read the [Contributing Guide](https://github.com/alpic-ai/skybridge/blob/main/CONTRIBUTING.md) before opening a PR.
|
|
142
169
|
|
|
143
|
-
|
|
170
|
+
Skybridge is released under the [MIT License](https://github.com/alpic-ai/skybridge/blob/main/LICENSE).
|
|
144
171
|
|
|
145
|
-
|
|
172
|
+
### Contributors
|
|
146
173
|
|
|
147
|
-
|
|
174
|
+
Built and maintained by [Harijoe](https://github.com/harijoe), [Fred Barthelet](https://github.com/fredericbarthelet), and the [Alpic](https://alpic.ai) team.
|
|
148
175
|
|
|
149
|
-
|
|
176
|
+
<a href="https://github.com/alpic-ai/skybridge/graphs/contributors">
|
|
177
|
+
<img src="https://contrib.rocks/image?repo=alpic-ai/skybridge" alt="Skybridge contributors">
|
|
178
|
+
</a>
|
package/dist/cli/header.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
export const Header = ({ version, children, }) => {
|
|
4
|
-
return (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u26F0", " ", "
|
|
4
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u26F0", " ", "Skybridge"] }), _jsxs(Text, { color: "cyan", children: [" v", version] }), children] }));
|
|
5
5
|
};
|
|
6
6
|
//# sourceMappingURL=header.js.map
|
package/dist/cli/header.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/cli/header.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAEhC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,OAAO,EACP,QAAQ,GAIT,EAAE,EAAE;IACH,OAAO,CACL,MAAC,GAAG,IAAC,YAAY,EAAE,CAAC,aAClB,MAAC,IAAI,IAAC,KAAK,EAAC,MAAM,EAAC,IAAI,6BACnB,IAAI,
|
|
1
|
+
{"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/cli/header.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAEhC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,OAAO,EACP,QAAQ,GAIT,EAAE,EAAE;IACH,OAAO,CACL,MAAC,GAAG,IAAC,YAAY,EAAE,CAAC,aAClB,MAAC,IAAI,IAAC,KAAK,EAAC,MAAM,EAAC,IAAI,6BACnB,IAAI,iBACD,EACP,MAAC,IAAI,IAAC,KAAK,EAAC,MAAM,mBAAI,OAAO,IAAQ,EACpC,QAAQ,IACL,CACP,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SpawnFn, TunnelManager } from "./tunnel.js";
|
|
2
|
+
export type TunnelControlServer = {
|
|
3
|
+
port: number;
|
|
4
|
+
manager: TunnelManager;
|
|
5
|
+
close: () => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
export declare function startTunnelControlServer(getPort: () => number, options?: {
|
|
8
|
+
spawn?: SpawnFn;
|
|
9
|
+
}): Promise<TunnelControlServer>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { TunnelManager } from "./tunnel.js";
|
|
3
|
+
import { createTunnelHandler } from "./tunnel-handler.js";
|
|
4
|
+
export async function startTunnelControlServer(getPort, options) {
|
|
5
|
+
const manager = new TunnelManager({ getPort, spawn: options?.spawn });
|
|
6
|
+
const server = http.createServer(createTunnelHandler(manager));
|
|
7
|
+
await new Promise((resolve, reject) => {
|
|
8
|
+
server.once("error", reject);
|
|
9
|
+
server.listen(0, "127.0.0.1", () => {
|
|
10
|
+
server.off("error", reject);
|
|
11
|
+
resolve();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
const address = server.address();
|
|
15
|
+
if (typeof address === "string" || address === null) {
|
|
16
|
+
server.close();
|
|
17
|
+
throw new Error("tunnel control server has no address");
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
port: address.port,
|
|
21
|
+
manager,
|
|
22
|
+
close: () => new Promise((resolve, reject) => {
|
|
23
|
+
manager.stop();
|
|
24
|
+
// Force any in-flight SSE connections to drop so server.close()
|
|
25
|
+
// doesn't hang indefinitely on subscribers that never end on their own.
|
|
26
|
+
server.closeAllConnections();
|
|
27
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=tunnel-control-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-control-server.js","sourceRoot":"","sources":["../../src/cli/tunnel-control-server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAgB,aAAa,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ1D,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAqB,EACrB,OAA6B;IAE7B,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACtE,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/D,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YACjC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC5B,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IACjC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACpD,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO;QACP,KAAK,EAAE,GAAG,EAAE,CACV,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACpC,OAAO,CAAC,IAAI,EAAE,CAAC;YACf,gEAAgE;YAChE,wEAAwE;YACxE,MAAM,CAAC,mBAAmB,EAAE,CAAC;YAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACzD,CAAC,CAAC;KACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { startTunnelControlServer } from "./tunnel-control-server.js";
|
|
3
|
+
let openControl;
|
|
4
|
+
afterEach(async () => {
|
|
5
|
+
await openControl?.close();
|
|
6
|
+
openControl = undefined;
|
|
7
|
+
});
|
|
8
|
+
describe("startTunnelControlServer", () => {
|
|
9
|
+
it("listens on a random loopback port and serves /__skybridge/tunnel/events", async () => {
|
|
10
|
+
const control = await startTunnelControlServer(() => 3000);
|
|
11
|
+
openControl = control;
|
|
12
|
+
expect(control.port).toBeGreaterThan(0);
|
|
13
|
+
const res = await fetch(`http://127.0.0.1:${control.port}/__skybridge/tunnel/events`);
|
|
14
|
+
expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
15
|
+
const reader = res.body.getReader();
|
|
16
|
+
const { value } = await reader.read();
|
|
17
|
+
const chunk = new TextDecoder().decode(value);
|
|
18
|
+
expect(chunk).toContain("event: state");
|
|
19
|
+
expect(chunk).toContain('"status":"idle"');
|
|
20
|
+
await reader.cancel();
|
|
21
|
+
});
|
|
22
|
+
it("two concurrent control servers get different ports", async () => {
|
|
23
|
+
const a = await startTunnelControlServer(() => 3000);
|
|
24
|
+
const b = await startTunnelControlServer(() => 4000);
|
|
25
|
+
try {
|
|
26
|
+
expect(a.port).not.toBe(b.port);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await a.close();
|
|
30
|
+
await b.close();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
it("close() shuts the listener", async () => {
|
|
34
|
+
const control = await startTunnelControlServer(() => 3000);
|
|
35
|
+
await control.close();
|
|
36
|
+
await expect(fetch(`http://127.0.0.1:${control.port}/__skybridge/tunnel/events`)).rejects.toThrow();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=tunnel-control-server.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-control-server.test.js","sourceRoot":"","sources":["../../src/cli/tunnel-control-server.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,IAAI,WAAuD,CAAC;AAC5D,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,MAAM,WAAW,EAAE,KAAK,EAAE,CAAC;IAC3B,WAAW,GAAG,SAAS,CAAC;AAC1B,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,MAAM,OAAO,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3D,WAAW,GAAG,OAAO,CAAC;QAEtB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAExC,MAAM,GAAG,GAAG,MAAM,KAAK,CACrB,oBAAoB,OAAO,CAAC,IAAI,4BAA4B,CAC7D,CAAC;QACF,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAEtE,MAAM,MAAM,GAAI,GAAG,CAAC,IAAmC,CAAC,SAAS,EAAE,CAAC;QACpE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACxC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAC3C,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,CAAC,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,CAAC,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACrD,IAAI,CAAC;YACH,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,OAAO,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACtB,MAAM,MAAM,CACV,KAAK,CAAC,oBAAoB,OAAO,CAAC,IAAI,4BAA4B,CAAC,CACpE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function createTunnelHandler(manager) {
|
|
2
|
+
return (req, res) => {
|
|
3
|
+
if (req.url === "/__skybridge/tunnel" && req.method === "POST") {
|
|
4
|
+
manager.start();
|
|
5
|
+
sendJson(res, 200, manager.getState());
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (req.url === "/__skybridge/tunnel" && req.method === "DELETE") {
|
|
9
|
+
manager.stop();
|
|
10
|
+
sendJson(res, 200, manager.getState());
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (req.url === "/__skybridge/tunnel/events" && req.method === "GET") {
|
|
14
|
+
writeSseHead(res);
|
|
15
|
+
writeSse(res, "state", manager.getState());
|
|
16
|
+
const onState = (s) => {
|
|
17
|
+
writeSse(res, "state", s);
|
|
18
|
+
};
|
|
19
|
+
const onActivity = (a) => {
|
|
20
|
+
writeSse(res, "activity", a);
|
|
21
|
+
};
|
|
22
|
+
manager.on("state", onState);
|
|
23
|
+
manager.on("activity", onActivity);
|
|
24
|
+
req.on("close", () => {
|
|
25
|
+
manager.off("state", onState);
|
|
26
|
+
manager.off("activity", onActivity);
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
res.writeHead(404).end();
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function sendJson(res, status, data) {
|
|
34
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
35
|
+
res.end(JSON.stringify(data));
|
|
36
|
+
}
|
|
37
|
+
function writeSseHead(res) {
|
|
38
|
+
res.writeHead(200, {
|
|
39
|
+
"Content-Type": "text/event-stream",
|
|
40
|
+
"Cache-Control": "no-cache, no-transform",
|
|
41
|
+
Connection: "keep-alive",
|
|
42
|
+
});
|
|
43
|
+
res.flushHeaders?.();
|
|
44
|
+
}
|
|
45
|
+
function writeSse(res, event, data) {
|
|
46
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=tunnel-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-handler.js","sourceRoot":"","sources":["../../src/cli/tunnel-handler.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,mBAAmB,CAAC,OAAsB;IACxD,OAAO,CAAC,GAAoB,EAAE,GAAmB,EAAQ,EAAE;QACzD,IAAI,GAAG,CAAC,GAAG,KAAK,qBAAqB,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC/D,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACvC,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,GAAG,KAAK,qBAAqB,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACjE,OAAO,CAAC,IAAI,EAAE,CAAC;YACf,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACvC,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,GAAG,KAAK,4BAA4B,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YACrE,YAAY,CAAC,GAAG,CAAC,CAAC;YAClB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC3C,MAAM,OAAO,GAAG,CAAC,CAAc,EAAE,EAAE;gBACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;YAC5B,CAAC,CAAC;YACF,MAAM,UAAU,GAAG,CAAC,CAAiB,EAAE,EAAE;gBACvC,QAAQ,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;YAC/B,CAAC,CAAC;YACF,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7B,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YACnC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBACnB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC9B,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IAC3B,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAa;IAClE,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC9D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,YAAY,CAAC,GAAmB;IACvC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;QACjB,cAAc,EAAE,mBAAmB;QACnC,eAAe,EAAE,wBAAwB;QACzC,UAAU,EAAE,YAAY;KACzB,CAAC,CAAC;IACH,GAAG,CAAC,YAAY,EAAE,EAAE,CAAC;AACvB,CAAC;AAED,SAAS,QAAQ,CAAC,GAAmB,EAAE,KAAa,EAAE,IAAa;IACjE,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,WAAW,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { TunnelManager } from "./tunnel.js";
|
|
6
|
+
import { createTunnelHandler } from "./tunnel-handler.js";
|
|
7
|
+
let openServer;
|
|
8
|
+
afterEach(() => openServer?.close());
|
|
9
|
+
function makeFakeChild() {
|
|
10
|
+
const child = new EventEmitter();
|
|
11
|
+
child.stdout = new Readable({ read() { } });
|
|
12
|
+
child.stderr = new Readable({ read() { } });
|
|
13
|
+
child.kill = vi.fn(() => true);
|
|
14
|
+
return child;
|
|
15
|
+
}
|
|
16
|
+
async function listen(handler) {
|
|
17
|
+
const server = http.createServer(handler);
|
|
18
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
19
|
+
const port = server.address().port;
|
|
20
|
+
return { port, server };
|
|
21
|
+
}
|
|
22
|
+
async function listenWithHandler() {
|
|
23
|
+
const child = makeFakeChild();
|
|
24
|
+
const manager = new TunnelManager({
|
|
25
|
+
getPort: () => 3000,
|
|
26
|
+
spawn: () => child,
|
|
27
|
+
});
|
|
28
|
+
const { port, server } = await listen(createTunnelHandler(manager));
|
|
29
|
+
openServer = server;
|
|
30
|
+
return { port, child, manager };
|
|
31
|
+
}
|
|
32
|
+
describe("createTunnelHandler", () => {
|
|
33
|
+
it("POST /__skybridge/tunnel starts the tunnel and returns the current state", async () => {
|
|
34
|
+
const { port, child } = await listenWithHandler();
|
|
35
|
+
const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
});
|
|
38
|
+
expect(res.status).toBe(200);
|
|
39
|
+
expect(await res.json()).toEqual({
|
|
40
|
+
status: "starting",
|
|
41
|
+
message: "Starting tunnel…",
|
|
42
|
+
});
|
|
43
|
+
expect(child.kill).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
it("POST /__skybridge/tunnel is idempotent — second call does not respawn", async () => {
|
|
46
|
+
const { port, manager } = await listenWithHandler();
|
|
47
|
+
const startSpy = vi.spyOn(manager, "start");
|
|
48
|
+
await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
});
|
|
51
|
+
await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
});
|
|
54
|
+
expect(startSpy).toHaveBeenCalledTimes(2);
|
|
55
|
+
// Manager.start() is internally idempotent (verified in tunnel.test.ts).
|
|
56
|
+
});
|
|
57
|
+
it("DELETE /__skybridge/tunnel stops the tunnel", async () => {
|
|
58
|
+
const { port, child } = await listenWithHandler();
|
|
59
|
+
await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
});
|
|
62
|
+
const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
63
|
+
method: "DELETE",
|
|
64
|
+
});
|
|
65
|
+
expect(res.status).toBe(200);
|
|
66
|
+
expect(await res.json()).toEqual({ status: "idle" });
|
|
67
|
+
expect(child.kill).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
it("GET /__skybridge/tunnel/events streams the current state on connect", async () => {
|
|
70
|
+
const { port, child } = await listenWithHandler();
|
|
71
|
+
await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
});
|
|
74
|
+
child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
|
|
75
|
+
const res = await fetch(`http://localhost:${port}/__skybridge/tunnel/events`);
|
|
76
|
+
expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
77
|
+
expect(res.body).toBeTruthy();
|
|
78
|
+
const reader = res.body.getReader();
|
|
79
|
+
const { value } = await reader.read();
|
|
80
|
+
const chunk = new TextDecoder().decode(value);
|
|
81
|
+
expect(chunk).toContain("event: state");
|
|
82
|
+
expect(chunk).toContain('"status":"connected"');
|
|
83
|
+
expect(chunk).toContain('"url":"https://abc.tunnel.example"');
|
|
84
|
+
await reader.cancel();
|
|
85
|
+
});
|
|
86
|
+
it("GET /__skybridge/tunnel/events sends the current error state on connect", async () => {
|
|
87
|
+
const { port, child } = await listenWithHandler();
|
|
88
|
+
await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
});
|
|
91
|
+
child.stderr.emit("data", Buffer.from("boom: tunnel auth failed\n"));
|
|
92
|
+
child.emit("close", 1);
|
|
93
|
+
const res = await fetch(`http://localhost:${port}/__skybridge/tunnel/events`);
|
|
94
|
+
expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
95
|
+
expect(res.body).toBeTruthy();
|
|
96
|
+
const reader = res.body.getReader();
|
|
97
|
+
const { value } = await reader.read();
|
|
98
|
+
const chunk = new TextDecoder().decode(value);
|
|
99
|
+
expect(chunk).toContain("event: state");
|
|
100
|
+
expect(chunk).toContain('"status":"error"');
|
|
101
|
+
expect(chunk).toContain("boom: tunnel auth failed");
|
|
102
|
+
await reader.cancel();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=tunnel-handler.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-handler.test.js","sourceRoot":"","sources":["../../src/cli/tunnel-handler.test.ts"],"names":[],"mappings":"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"}
|