skybridge 0.0.0-dev.f381d90 → 0.0.0-dev.f38d432

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 (165) hide show
  1. package/README.md +8 -8
  2. package/dist/cli/header.js +1 -1
  3. package/dist/cli/header.js.map +1 -1
  4. package/dist/cli/tunnel-control-server.d.ts +9 -0
  5. package/dist/cli/tunnel-control-server.js +31 -0
  6. package/dist/cli/tunnel-control-server.js.map +1 -0
  7. package/dist/cli/tunnel-control-server.test.js +39 -0
  8. package/dist/cli/tunnel-control-server.test.js.map +1 -0
  9. package/dist/cli/tunnel-handler.d.ts +3 -0
  10. package/dist/cli/tunnel-handler.js +48 -0
  11. package/dist/cli/tunnel-handler.js.map +1 -0
  12. package/dist/cli/tunnel-handler.test.js +105 -0
  13. package/dist/cli/tunnel-handler.test.js.map +1 -0
  14. package/dist/cli/tunnel.d.ts +57 -0
  15. package/dist/cli/tunnel.js +154 -0
  16. package/dist/cli/tunnel.js.map +1 -0
  17. package/dist/cli/tunnel.test.js +190 -0
  18. package/dist/cli/tunnel.test.js.map +1 -0
  19. package/dist/cli/use-nodemon.js +2 -2
  20. package/dist/cli/use-nodemon.js.map +1 -1
  21. package/dist/cli/use-tunnel.d.ts +12 -5
  22. package/dist/cli/use-tunnel.js +102 -63
  23. package/dist/cli/use-tunnel.js.map +1 -1
  24. package/dist/cli/use-typescript-check.d.ts +1 -0
  25. package/dist/cli/use-typescript-check.js +41 -6
  26. package/dist/cli/use-typescript-check.js.map +1 -1
  27. package/dist/commands/build.js +28 -7
  28. package/dist/commands/build.js.map +1 -1
  29. package/dist/commands/dev.d.ts +1 -0
  30. package/dist/commands/dev.js +32 -3
  31. package/dist/commands/dev.js.map +1 -1
  32. package/dist/commands/start.js +7 -10
  33. package/dist/commands/start.js.map +1 -1
  34. package/dist/server/asset-base-url-transform-plugin.js +1 -1
  35. package/dist/server/asset-base-url-transform-plugin.js.map +1 -1
  36. package/dist/server/asset-base-url-transform-plugin.test.js +29 -0
  37. package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -1
  38. package/dist/server/content-helpers.d.ts +27 -0
  39. package/dist/server/content-helpers.js +46 -0
  40. package/dist/server/content-helpers.js.map +1 -0
  41. package/dist/server/content-helpers.test.d.ts +1 -0
  42. package/dist/server/content-helpers.test.js +70 -0
  43. package/dist/server/content-helpers.test.js.map +1 -0
  44. package/dist/server/express.js +21 -3
  45. package/dist/server/express.js.map +1 -1
  46. package/dist/server/express.test.js +98 -2
  47. package/dist/server/express.test.js.map +1 -1
  48. package/dist/server/index.d.ts +4 -3
  49. package/dist/server/index.js +3 -2
  50. package/dist/server/index.js.map +1 -1
  51. package/dist/server/inferUtilityTypes.d.ts +6 -6
  52. package/dist/server/middleware.test.js +12 -9
  53. package/dist/server/middleware.test.js.map +1 -1
  54. package/dist/server/server.d.ts +95 -72
  55. package/dist/server/server.js +208 -74
  56. package/dist/server/server.js.map +1 -1
  57. package/dist/server/templateHelper.d.ts +5 -5
  58. package/dist/server/templates/development.hbs +2 -2
  59. package/dist/server/templates/production.hbs +1 -1
  60. package/dist/server/tunnel-proxy-router.d.ts +7 -0
  61. package/dist/server/tunnel-proxy-router.js +110 -0
  62. package/dist/server/tunnel-proxy-router.js.map +1 -0
  63. package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
  64. package/dist/server/tunnel-proxy-router.test.js +229 -0
  65. package/dist/server/tunnel-proxy-router.test.js.map +1 -0
  66. package/dist/server/viewsDevServer.d.ts +14 -0
  67. package/dist/server/viewsDevServer.js +45 -0
  68. package/dist/server/viewsDevServer.js.map +1 -0
  69. package/dist/test/utils.d.ts +13 -21
  70. package/dist/test/utils.js +42 -37
  71. package/dist/test/utils.js.map +1 -1
  72. package/dist/test/view.test.d.ts +1 -0
  73. package/dist/test/view.test.js +523 -0
  74. package/dist/test/view.test.js.map +1 -0
  75. package/dist/web/bridges/apps-sdk/adaptor.d.ts +5 -3
  76. package/dist/web/bridges/apps-sdk/adaptor.js +32 -14
  77. package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -1
  78. package/dist/web/bridges/apps-sdk/types.d.ts +10 -5
  79. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  80. package/dist/web/bridges/mcp-app/adaptor.d.ts +12 -6
  81. package/dist/web/bridges/mcp-app/adaptor.js +30 -24
  82. package/dist/web/bridges/mcp-app/adaptor.js.map +1 -1
  83. package/dist/web/bridges/types.d.ts +14 -8
  84. package/dist/web/components/modal-provider.js +1 -1
  85. package/dist/web/components/modal-provider.js.map +1 -1
  86. package/dist/web/create-store.js +9 -9
  87. package/dist/web/create-store.js.map +1 -1
  88. package/dist/web/create-store.test.js +14 -16
  89. package/dist/web/create-store.test.js.map +1 -1
  90. package/dist/web/data-llm.d.ts +1 -1
  91. package/dist/web/data-llm.js +3 -3
  92. package/dist/web/data-llm.js.map +1 -1
  93. package/dist/web/data-llm.test.js +22 -22
  94. package/dist/web/data-llm.test.js.map +1 -1
  95. package/dist/web/generate-helpers.d.ts +20 -18
  96. package/dist/web/generate-helpers.js +20 -18
  97. package/dist/web/generate-helpers.js.map +1 -1
  98. package/dist/web/generate-helpers.test-d.js +26 -26
  99. package/dist/web/generate-helpers.test-d.js.map +1 -1
  100. package/dist/web/helpers/state.d.ts +2 -2
  101. package/dist/web/helpers/state.js +11 -11
  102. package/dist/web/helpers/state.js.map +1 -1
  103. package/dist/web/helpers/state.test.js +9 -9
  104. package/dist/web/helpers/state.test.js.map +1 -1
  105. package/dist/web/hooks/index.d.ts +1 -1
  106. package/dist/web/hooks/index.js +1 -1
  107. package/dist/web/hooks/index.js.map +1 -1
  108. package/dist/web/hooks/use-files.d.ts +2 -1
  109. package/dist/web/hooks/use-files.js +1 -0
  110. package/dist/web/hooks/use-files.js.map +1 -1
  111. package/dist/web/hooks/use-files.test.js +22 -2
  112. package/dist/web/hooks/use-files.test.js.map +1 -1
  113. package/dist/web/hooks/use-request-modal.d.ts +1 -1
  114. package/dist/web/hooks/use-request-modal.js +4 -4
  115. package/dist/web/hooks/use-request-modal.js.map +1 -1
  116. package/dist/web/hooks/use-request-modal.test.js +1 -1
  117. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  118. package/dist/web/hooks/use-view-state.d.ts +4 -0
  119. package/dist/web/hooks/use-view-state.js +32 -0
  120. package/dist/web/hooks/use-view-state.js.map +1 -0
  121. package/dist/web/hooks/use-view-state.test.d.ts +1 -0
  122. package/dist/web/hooks/{use-widget-state.test.js → use-view-state.test.js} +17 -17
  123. package/dist/web/hooks/use-view-state.test.js.map +1 -0
  124. package/dist/web/index.d.ts +1 -2
  125. package/dist/web/index.js +1 -2
  126. package/dist/web/index.js.map +1 -1
  127. package/dist/web/mount-view.d.ts +1 -0
  128. package/dist/web/{mount-widget.js → mount-view.js} +2 -2
  129. package/dist/web/mount-view.js.map +1 -0
  130. package/dist/web/plugin/plugin.d.ts +4 -1
  131. package/dist/web/plugin/plugin.js +134 -25
  132. package/dist/web/plugin/plugin.js.map +1 -1
  133. package/dist/web/plugin/scan-views.d.ts +16 -0
  134. package/dist/web/plugin/scan-views.js +88 -0
  135. package/dist/web/plugin/scan-views.js.map +1 -0
  136. package/dist/web/plugin/scan-views.test.d.ts +1 -0
  137. package/dist/web/plugin/scan-views.test.js +99 -0
  138. package/dist/web/plugin/scan-views.test.js.map +1 -0
  139. package/dist/web/plugin/validate-view.d.ts +1 -0
  140. package/dist/web/plugin/validate-view.js +9 -0
  141. package/dist/web/plugin/validate-view.js.map +1 -0
  142. package/dist/web/plugin/validate-view.test.d.ts +1 -0
  143. package/dist/web/plugin/validate-view.test.js +24 -0
  144. package/dist/web/plugin/validate-view.test.js.map +1 -0
  145. package/package.json +22 -16
  146. package/tsconfig.base.json +2 -0
  147. package/dist/server/widgetsDevServer.d.ts +0 -13
  148. package/dist/server/widgetsDevServer.js +0 -52
  149. package/dist/server/widgetsDevServer.js.map +0 -1
  150. package/dist/test/widget.test.js +0 -303
  151. package/dist/test/widget.test.js.map +0 -1
  152. package/dist/web/hooks/use-widget-state.d.ts +0 -4
  153. package/dist/web/hooks/use-widget-state.js +0 -32
  154. package/dist/web/hooks/use-widget-state.js.map +0 -1
  155. package/dist/web/hooks/use-widget-state.test.js.map +0 -1
  156. package/dist/web/mount-widget.d.ts +0 -1
  157. package/dist/web/mount-widget.js.map +0 -1
  158. package/dist/web/plugin/validate-widget.d.ts +0 -5
  159. package/dist/web/plugin/validate-widget.js +0 -27
  160. package/dist/web/plugin/validate-widget.js.map +0 -1
  161. package/dist/web/plugin/validate-widget.test.js +0 -42
  162. package/dist/web/plugin/validate-widget.test.js.map +0 -1
  163. /package/dist/{test/widget.test.d.ts → cli/tunnel-control-server.test.d.ts} +0 -0
  164. /package/dist/{web/hooks/use-widget-state.test.d.ts → cli/tunnel-handler.test.d.ts} +0 -0
  165. /package/dist/{web/plugin/validate-widget.test.d.ts → cli/tunnel.test.d.ts} +0 -0
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Build ChatGPT & MCP Apps. The Modern TypeScript Way.**
8
8
 
9
- The fullstack TypeScript framework for AI-embedded widgets.<br />
9
+ The fullstack TypeScript framework for AI-embedded views.<br />
10
10
  **Type-safe. React-powered. Platform-agnostic.**
11
11
 
12
12
  <br />
@@ -31,8 +31,8 @@ ChatGPT Apps and MCP Apps let you embed **rich, interactive UIs** directly in AI
31
31
 
32
32
  | | |
33
33
  |:--|:--|
34
- | 🌐 **Write once, run everywhere** — Skybridge works seamlessly with ChatGPT (Apps SDK) and MCP-compatible clients. | ✅ **End-to-End Type Safety** — tRPC-style inference from server to widget. Autocomplete everywhere. |
35
- | 🔄 **Widget-to-Model Sync** — Keep the model aware of UI state with `data-llm`. Dual surfaces, one source of truth. | ⚒️ **React Query-style Hooks** — `isPending`, `isError`, callbacks. State management you already know. |
34
+ | 🌐 **Write once, run everywhere** — Skybridge works seamlessly with ChatGPT (Apps SDK) and MCP-compatible clients. | ✅ **End-to-End Type Safety** — tRPC-style inference from server to view. Autocomplete everywhere. |
35
+ | 🔄 **View-to-Model Sync** — Keep the model aware of UI state with `data-llm`. Dual surfaces, one source of truth. | ⚒️ **React Query-style Hooks** — `isPending`, `isError`, callbacks. State management you already know. |
36
36
  | 👨‍💻 **Full dev environment** — HMR, debug traces, and local devtools. | 📦 **Showcase Examples** — Production-ready examples to learn from and build upon. |
37
37
 
38
38
  <br />
@@ -67,7 +67,7 @@ deno add skybridge
67
67
 
68
68
  Skybridge is a fullstack framework with unified server and client modules:
69
69
 
70
- - **`skybridge/server`** — Define tools and widgets with full type inference. Extends the MCP SDK.
70
+ - **`skybridge/server`** — Define tools and views with full type inference. Extends the MCP SDK.
71
71
  - **`skybridge/web`** — React hooks that consume your server types. Works with Apps SDK (ChatGPT) and MCP Apps.
72
72
  - **Dev Environment** — Vite plugin with HMR, DevTools emulator, and optimized builds.
73
73
 
@@ -76,7 +76,7 @@ Skybridge is a fullstack framework with unified server and client modules:
76
76
  ```ts
77
77
  import { McpServer } from "skybridge/server";
78
78
 
79
- server.registerWidget("flights", {}, {
79
+ server.registerView("flights", {}, {
80
80
  inputSchema: { destination: z.string() },
81
81
  }, async ({ destination }) => {
82
82
  const flights = await searchFlights(destination);
@@ -84,12 +84,12 @@ server.registerWidget("flights", {}, {
84
84
  });
85
85
  ```
86
86
 
87
- ### Widget
87
+ ### View
88
88
 
89
89
  ```tsx
90
90
  import { useToolInfo } from "skybridge/web";
91
91
 
92
- function FlightsWidget() {
92
+ function FlightsView() {
93
93
  const { output } = useToolInfo();
94
94
 
95
95
  return output.structuredContent.flights.map(flight =>
@@ -104,7 +104,7 @@ function FlightsWidget() {
104
104
 
105
105
  - **Live Reload** — Vite HMR. See changes instantly without reinstalling.
106
106
  - **Typed Hooks** — Full autocomplete for tools, inputs, outputs.
107
- - **Widget → Tool Calls** — Trigger server actions from UI.
107
+ - **View → Tool Calls** — Trigger server actions from UI.
108
108
  - **Dual Surface Sync** — Keep model aware of what users see with `data-llm`.
109
109
  - **React Query-style API** — `isPending`, `isError`, callbacks.
110
110
  - **Platform Agnostic** — Works with ChatGPT (Apps SDK) and MCP Apps clients (Goose, VSCode, etc.).
@@ -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", " ", "Welcome to Skybridge"] }), _jsxs(Text, { color: "cyan", children: [" v", version] }), children] }));
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
@@ -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,4BACD,EACP,MAAC,IAAI,IAAC,KAAK,EAAC,MAAM,mBAAI,OAAO,IAAQ,EACpC,QAAQ,IACL,CACP,CAAC;AACJ,CAAC,CAAC"}
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,3 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { TunnelManager } from "./tunnel.js";
3
+ export declare function createTunnelHandler(manager: TunnelManager): (req: IncomingMessage, res: ServerResponse) => void;
@@ -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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"}
@@ -0,0 +1,57 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { Readable } from "node:stream";
3
+ export type TunnelState = {
4
+ status: "idle";
5
+ } | {
6
+ status: "starting";
7
+ message: string;
8
+ } | {
9
+ status: "connected";
10
+ url: string;
11
+ } | {
12
+ status: "error";
13
+ message: string;
14
+ };
15
+ export type ParsedStdoutEvent = {
16
+ kind: "connected";
17
+ url: string;
18
+ } | {
19
+ kind: "starting";
20
+ message: string;
21
+ };
22
+ export declare function parseStdoutLine(line: string): ParsedStdoutEvent | null;
23
+ export type TunnelActivity = {
24
+ time: string;
25
+ text: string;
26
+ level: "log" | "error";
27
+ };
28
+ export type TunnelChildProcess = {
29
+ stdout: Pick<Readable, "on"> | null;
30
+ stderr: Pick<Readable, "on"> | null;
31
+ kill: (signal?: NodeJS.Signals | number) => boolean;
32
+ on(event: "error", listener: (err: Error) => void): unknown;
33
+ on(event: "close", listener: (code: number | null) => void): unknown;
34
+ };
35
+ export type SpawnFn = (port: number) => TunnelChildProcess;
36
+ export declare class TunnelManager extends EventEmitter {
37
+ private state;
38
+ private child;
39
+ private timeout;
40
+ private stderrBuffer;
41
+ private connected;
42
+ private readonly getPort;
43
+ private readonly spawnFn;
44
+ constructor(opts: {
45
+ getPort: () => number;
46
+ spawn?: SpawnFn;
47
+ });
48
+ getState(): TunnelState;
49
+ subscribe(listener: (state: TunnelState) => void): () => void;
50
+ start(): void;
51
+ stop(): void;
52
+ private handleStdout;
53
+ private handleStderr;
54
+ private setState;
55
+ private emitActivity;
56
+ private clearConnectTimeout;
57
+ }
@@ -0,0 +1,154 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ const FORWARDING_RE = /Forwarding:\s+(https?:\/\/\S+)\s*->\s*(\S+)/;
4
+ export function parseStdoutLine(line) {
5
+ const trimmed = line.trim();
6
+ if (!trimmed) {
7
+ return null;
8
+ }
9
+ const match = trimmed.match(FORWARDING_RE);
10
+ if (match?.[1]) {
11
+ return { kind: "connected", url: match[1].replace(/\/$/, "") };
12
+ }
13
+ return { kind: "starting", message: trimmed };
14
+ }
15
+ const CONNECT_TIMEOUT_MS = 60_000;
16
+ const STDERR_BUFFER_BYTES = 1024;
17
+ const defaultSpawn = (port) => nodeSpawn("npx", ["--yes", "alpic", "tunnel", "--port", String(port), "--plain"], {
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ });
20
+ export class TunnelManager extends EventEmitter {
21
+ state = { status: "idle" };
22
+ child = null;
23
+ timeout = null;
24
+ stderrBuffer = "";
25
+ connected = false;
26
+ getPort;
27
+ spawnFn;
28
+ constructor(opts) {
29
+ super();
30
+ this.getPort = opts.getPort;
31
+ this.spawnFn = opts.spawn ?? defaultSpawn;
32
+ // Multiple SSE subscribers (CLI, devtools, ad-hoc curl) can each register
33
+ // state + activity listeners; the default cap of 10 is easy to hit.
34
+ this.setMaxListeners(0);
35
+ }
36
+ getState() {
37
+ return this.state;
38
+ }
39
+ subscribe(listener) {
40
+ listener(this.state);
41
+ this.on("state", listener);
42
+ return () => {
43
+ this.off("state", listener);
44
+ };
45
+ }
46
+ start() {
47
+ if (this.state.status === "starting" || this.state.status === "connected") {
48
+ return;
49
+ }
50
+ this.connected = false;
51
+ this.stderrBuffer = "";
52
+ this.setState({ status: "starting", message: "Starting tunnel…" });
53
+ const child = this.spawnFn(this.getPort());
54
+ this.child = child;
55
+ this.timeout = setTimeout(() => {
56
+ if (!this.connected) {
57
+ this.setState({
58
+ status: "error",
59
+ message: "Tunnel connection timed out after one minute",
60
+ });
61
+ // Detach before killing so the imminent `close` event is treated as
62
+ // stale and does not overwrite the timeout error message.
63
+ this.child = null;
64
+ child.kill();
65
+ }
66
+ }, CONNECT_TIMEOUT_MS);
67
+ child.stdout?.on("data", (data) => {
68
+ this.handleStdout(data);
69
+ });
70
+ child.stderr?.on("data", (data) => {
71
+ this.handleStderr(data);
72
+ });
73
+ child.on("error", (err) => {
74
+ // Stale event from a child we've already replaced via stop()+start().
75
+ if (child !== this.child) {
76
+ return;
77
+ }
78
+ this.clearConnectTimeout();
79
+ this.setState({ status: "error", message: err.message });
80
+ });
81
+ child.on("close", (code) => {
82
+ // Stale event from a child we've already replaced via stop()+start().
83
+ if (child !== this.child) {
84
+ return;
85
+ }
86
+ this.clearConnectTimeout();
87
+ if (code !== 0 && code !== null) {
88
+ const detail = this.stderrBuffer.trim() || `exited with code ${code}`;
89
+ this.setState({ status: "error", message: detail });
90
+ }
91
+ else {
92
+ this.setState({ status: "idle" });
93
+ }
94
+ this.child = null;
95
+ });
96
+ }
97
+ stop() {
98
+ this.clearConnectTimeout();
99
+ if (this.child) {
100
+ this.child.kill();
101
+ this.child = null;
102
+ }
103
+ this.setState({ status: "idle" });
104
+ }
105
+ handleStdout(data) {
106
+ const lines = data.toString().split("\n");
107
+ for (const raw of lines) {
108
+ const parsed = parseStdoutLine(raw);
109
+ if (!parsed) {
110
+ continue;
111
+ }
112
+ if (parsed.kind === "connected") {
113
+ this.connected = true;
114
+ this.clearConnectTimeout();
115
+ this.setState({ status: "connected", url: parsed.url });
116
+ }
117
+ else if (this.connected) {
118
+ this.emitActivity(parsed.message, "log");
119
+ }
120
+ else {
121
+ this.setState({ status: "starting", message: parsed.message });
122
+ }
123
+ }
124
+ }
125
+ handleStderr(data) {
126
+ const text = data.toString().trim();
127
+ if (!text) {
128
+ return;
129
+ }
130
+ this.stderrBuffer = (this.stderrBuffer + text).slice(-STDERR_BUFFER_BYTES);
131
+ for (const line of text.split("\n").filter(Boolean)) {
132
+ this.emitActivity(line, "error");
133
+ }
134
+ }
135
+ setState(next) {
136
+ this.state = next;
137
+ this.emit("state", next);
138
+ }
139
+ emitActivity(text, level) {
140
+ const activity = {
141
+ time: new Date().toISOString(),
142
+ text,
143
+ level,
144
+ };
145
+ this.emit("activity", activity);
146
+ }
147
+ clearConnectTimeout() {
148
+ if (this.timeout) {
149
+ clearTimeout(this.timeout);
150
+ this.timeout = null;
151
+ }
152
+ }
153
+ }
154
+ //# sourceMappingURL=tunnel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../../src/cli/tunnel.ts"],"names":[],"mappings":"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"}