gitspace 0.2.0-rc.1

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 (318) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gitspace/bundle.json +50 -0
  3. package/.gitspace/select/01-status.sh +40 -0
  4. package/.gitspace/setup/01-install-deps.sh +12 -0
  5. package/.gitspace/setup/02-typecheck.sh +16 -0
  6. package/AGENTS.md +439 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +25 -0
  9. package/README.md +607 -0
  10. package/bin/gssh +62 -0
  11. package/bun.lock +647 -0
  12. package/docs/CONNECTION.md +623 -0
  13. package/docs/GATEWAY-WORKER.md +319 -0
  14. package/docs/GETTING-STARTED.md +448 -0
  15. package/docs/GITSPACE-PLATFORM.md +1819 -0
  16. package/docs/INFRASTRUCTURE.md +1347 -0
  17. package/docs/PROTOCOL.md +619 -0
  18. package/docs/QUICKSTART.md +174 -0
  19. package/docs/RELAY.md +327 -0
  20. package/docs/REMOTE-DESIGN.md +549 -0
  21. package/docs/ROADMAP.md +564 -0
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
  23. package/docs/STACK-DESIGN.md +588 -0
  24. package/docs/UNIFIED_ARCHITECTURE.md +292 -0
  25. package/experiments/pty-benchmark.ts +148 -0
  26. package/experiments/pty-latency.ts +100 -0
  27. package/experiments/router/client.ts +199 -0
  28. package/experiments/router/protocol.ts +74 -0
  29. package/experiments/router/router.ts +217 -0
  30. package/experiments/router/session.ts +180 -0
  31. package/experiments/router/test.ts +133 -0
  32. package/experiments/socket-bandwidth.ts +77 -0
  33. package/homebrew/gitspace.rb +45 -0
  34. package/landing-page/ATTRIBUTIONS.md +3 -0
  35. package/landing-page/README.md +11 -0
  36. package/landing-page/bun.lock +801 -0
  37. package/landing-page/guidelines/Guidelines.md +61 -0
  38. package/landing-page/index.html +37 -0
  39. package/landing-page/package.json +90 -0
  40. package/landing-page/postcss.config.mjs +15 -0
  41. package/landing-page/public/_redirects +1 -0
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +53 -0
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
  45. package/landing-page/src/app/components/ui/accordion.tsx +66 -0
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
  47. package/landing-page/src/app/components/ui/alert.tsx +66 -0
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
  49. package/landing-page/src/app/components/ui/avatar.tsx +53 -0
  50. package/landing-page/src/app/components/ui/badge.tsx +46 -0
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
  52. package/landing-page/src/app/components/ui/button.tsx +57 -0
  53. package/landing-page/src/app/components/ui/calendar.tsx +75 -0
  54. package/landing-page/src/app/components/ui/card.tsx +92 -0
  55. package/landing-page/src/app/components/ui/carousel.tsx +241 -0
  56. package/landing-page/src/app/components/ui/chart.tsx +353 -0
  57. package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
  58. package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
  59. package/landing-page/src/app/components/ui/command.tsx +177 -0
  60. package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
  61. package/landing-page/src/app/components/ui/dialog.tsx +135 -0
  62. package/landing-page/src/app/components/ui/drawer.tsx +132 -0
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
  64. package/landing-page/src/app/components/ui/form.tsx +168 -0
  65. package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
  66. package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
  67. package/landing-page/src/app/components/ui/input.tsx +21 -0
  68. package/landing-page/src/app/components/ui/label.tsx +24 -0
  69. package/landing-page/src/app/components/ui/menubar.tsx +276 -0
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
  71. package/landing-page/src/app/components/ui/pagination.tsx +127 -0
  72. package/landing-page/src/app/components/ui/popover.tsx +48 -0
  73. package/landing-page/src/app/components/ui/progress.tsx +31 -0
  74. package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
  75. package/landing-page/src/app/components/ui/resizable.tsx +56 -0
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
  77. package/landing-page/src/app/components/ui/select.tsx +189 -0
  78. package/landing-page/src/app/components/ui/separator.tsx +28 -0
  79. package/landing-page/src/app/components/ui/sheet.tsx +139 -0
  80. package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
  81. package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
  82. package/landing-page/src/app/components/ui/slider.tsx +63 -0
  83. package/landing-page/src/app/components/ui/sonner.tsx +25 -0
  84. package/landing-page/src/app/components/ui/switch.tsx +31 -0
  85. package/landing-page/src/app/components/ui/table.tsx +116 -0
  86. package/landing-page/src/app/components/ui/tabs.tsx +66 -0
  87. package/landing-page/src/app/components/ui/textarea.tsx +18 -0
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
  89. package/landing-page/src/app/components/ui/toggle.tsx +47 -0
  90. package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
  91. package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
  92. package/landing-page/src/app/components/ui/utils.ts +6 -0
  93. package/landing-page/src/components/docs/DocsContent.tsx +718 -0
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
  95. package/landing-page/src/components/landing/CTA.tsx +59 -0
  96. package/landing-page/src/components/landing/Comparison.tsx +84 -0
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
  98. package/landing-page/src/components/landing/Features.tsx +201 -0
  99. package/landing-page/src/components/landing/Hero.tsx +142 -0
  100. package/landing-page/src/components/landing/Pricing.tsx +140 -0
  101. package/landing-page/src/components/landing/Roadmap.tsx +86 -0
  102. package/landing-page/src/components/landing/Security.tsx +81 -0
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
  104. package/landing-page/src/components/landing/UseCases.tsx +55 -0
  105. package/landing-page/src/components/landing/Workflow.tsx +101 -0
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
  107. package/landing-page/src/components/layout/Footer.tsx +55 -0
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
  109. package/landing-page/src/components/ui/badge.tsx +39 -0
  110. package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
  111. package/landing-page/src/components/ui/button.tsx +57 -0
  112. package/landing-page/src/components/ui/card.tsx +79 -0
  113. package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
  114. package/landing-page/src/components/ui/separator.tsx +28 -0
  115. package/landing-page/src/lib/utils.ts +6 -0
  116. package/landing-page/src/main.tsx +10 -0
  117. package/landing-page/src/pages/Dashboard.tsx +133 -0
  118. package/landing-page/src/pages/DocsPage.tsx +79 -0
  119. package/landing-page/src/pages/LandingPage.tsx +31 -0
  120. package/landing-page/src/pages/TerminalView.tsx +106 -0
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +3 -0
  123. package/landing-page/src/styles/tailwind.css +4 -0
  124. package/landing-page/src/styles/theme.css +181 -0
  125. package/landing-page/vite.config.ts +19 -0
  126. package/npm/darwin-arm64/bin/gssh +0 -0
  127. package/npm/darwin-arm64/package.json +20 -0
  128. package/package.json +74 -0
  129. package/scripts/build.ts +284 -0
  130. package/scripts/release.ts +140 -0
  131. package/src/__tests__/test-utils.ts +298 -0
  132. package/src/commands/__tests__/serve-messages.test.ts +190 -0
  133. package/src/commands/access.ts +298 -0
  134. package/src/commands/add.ts +452 -0
  135. package/src/commands/auth.ts +364 -0
  136. package/src/commands/connect.ts +287 -0
  137. package/src/commands/directory.ts +16 -0
  138. package/src/commands/host.ts +396 -0
  139. package/src/commands/identity.ts +184 -0
  140. package/src/commands/list.ts +200 -0
  141. package/src/commands/relay.ts +315 -0
  142. package/src/commands/remove.ts +241 -0
  143. package/src/commands/serve.ts +1493 -0
  144. package/src/commands/share.ts +456 -0
  145. package/src/commands/status.ts +125 -0
  146. package/src/commands/switch.ts +353 -0
  147. package/src/commands/tmux.ts +317 -0
  148. package/src/core/__tests__/access.test.ts +240 -0
  149. package/src/core/access.ts +277 -0
  150. package/src/core/bundle.ts +342 -0
  151. package/src/core/config.ts +510 -0
  152. package/src/core/git.ts +317 -0
  153. package/src/core/github.ts +151 -0
  154. package/src/core/identity.ts +631 -0
  155. package/src/core/linear.ts +225 -0
  156. package/src/core/shell.ts +161 -0
  157. package/src/core/trusted-relays.ts +315 -0
  158. package/src/index.ts +821 -0
  159. package/src/lib/remote-session/index.ts +7 -0
  160. package/src/lib/remote-session/protocol.ts +267 -0
  161. package/src/lib/remote-session/session-handler.ts +581 -0
  162. package/src/lib/remote-session/workspace-scanner.ts +167 -0
  163. package/src/lib/tmux-lite/README.md +81 -0
  164. package/src/lib/tmux-lite/cli.ts +796 -0
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
  166. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
  167. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
  171. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
  172. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
  173. package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
  174. package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
  175. package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
  176. package/src/lib/tmux-lite/crypto/frames.ts +141 -0
  177. package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
  178. package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
  179. package/src/lib/tmux-lite/crypto/identity.ts +286 -0
  180. package/src/lib/tmux-lite/crypto/index.ts +51 -0
  181. package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
  182. package/src/lib/tmux-lite/crypto/invites.ts +215 -0
  183. package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
  184. package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
  185. package/src/lib/tmux-lite/crypto/keys.ts +47 -0
  186. package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
  187. package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
  188. package/src/lib/tmux-lite/handshake-handler.ts +451 -0
  189. package/src/lib/tmux-lite/protocol.test.ts +307 -0
  190. package/src/lib/tmux-lite/protocol.ts +266 -0
  191. package/src/lib/tmux-lite/relay-client.ts +506 -0
  192. package/src/lib/tmux-lite/server.ts +1250 -0
  193. package/src/lib/tmux-lite/shell-integration.sh +37 -0
  194. package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
  195. package/src/lib/tmux-lite/terminal-queries.ts +49 -0
  196. package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
  197. package/src/relay/__tests__/helpers/auth.ts +354 -0
  198. package/src/relay/__tests__/helpers/ports.ts +51 -0
  199. package/src/relay/__tests__/protocol-validation.test.ts +265 -0
  200. package/src/relay/authorization.ts +303 -0
  201. package/src/relay/embedded-assets.generated.d.ts +15 -0
  202. package/src/relay/identity.ts +352 -0
  203. package/src/relay/index.ts +57 -0
  204. package/src/relay/pipes.test.ts +427 -0
  205. package/src/relay/pipes.ts +195 -0
  206. package/src/relay/protocol.ts +804 -0
  207. package/src/relay/registries.test.ts +437 -0
  208. package/src/relay/registries.ts +593 -0
  209. package/src/relay/server.test.ts +1323 -0
  210. package/src/relay/server.ts +1092 -0
  211. package/src/relay/signing.ts +238 -0
  212. package/src/relay/types.ts +69 -0
  213. package/src/serve/client-session-manager.ts +622 -0
  214. package/src/serve/daemon.ts +497 -0
  215. package/src/serve/pty-session.ts +236 -0
  216. package/src/serve/types.ts +169 -0
  217. package/src/shared/components/Flow.tsx +453 -0
  218. package/src/shared/components/Flow.tui.tsx +343 -0
  219. package/src/shared/components/Flow.web.tsx +442 -0
  220. package/src/shared/components/Inbox.tsx +446 -0
  221. package/src/shared/components/Inbox.tui.tsx +262 -0
  222. package/src/shared/components/Inbox.web.tsx +329 -0
  223. package/src/shared/components/MachineList.tsx +187 -0
  224. package/src/shared/components/MachineList.tui.tsx +161 -0
  225. package/src/shared/components/MachineList.web.tsx +210 -0
  226. package/src/shared/components/ProjectList.tsx +176 -0
  227. package/src/shared/components/ProjectList.tui.tsx +109 -0
  228. package/src/shared/components/ProjectList.web.tsx +143 -0
  229. package/src/shared/components/SpacesBrowser.tsx +332 -0
  230. package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
  231. package/src/shared/components/SpacesBrowser.web.tsx +221 -0
  232. package/src/shared/components/index.ts +103 -0
  233. package/src/shared/hooks/index.ts +16 -0
  234. package/src/shared/hooks/useNavigation.ts +226 -0
  235. package/src/shared/index.ts +122 -0
  236. package/src/shared/providers/LocalMachineProvider.ts +425 -0
  237. package/src/shared/providers/MachineProvider.ts +165 -0
  238. package/src/shared/providers/RemoteMachineProvider.ts +444 -0
  239. package/src/shared/providers/index.ts +26 -0
  240. package/src/shared/types.ts +145 -0
  241. package/src/tui/adapters.ts +120 -0
  242. package/src/tui/app.tsx +1816 -0
  243. package/src/tui/components/Terminal.tsx +580 -0
  244. package/src/tui/hooks/index.ts +35 -0
  245. package/src/tui/hooks/useAppState.ts +314 -0
  246. package/src/tui/hooks/useDaemonStatus.ts +174 -0
  247. package/src/tui/hooks/useInboxTUI.ts +113 -0
  248. package/src/tui/hooks/useRemoteMachines.ts +209 -0
  249. package/src/tui/index.ts +24 -0
  250. package/src/tui/state.ts +299 -0
  251. package/src/tui/terminal-bracketed-paste.test.ts +45 -0
  252. package/src/tui/terminal-bracketed-paste.ts +47 -0
  253. package/src/types/bundle.ts +112 -0
  254. package/src/types/config.ts +89 -0
  255. package/src/types/errors.ts +206 -0
  256. package/src/types/identity.ts +284 -0
  257. package/src/types/workspace-fuzzy.ts +49 -0
  258. package/src/types/workspace.ts +151 -0
  259. package/src/utils/bun-socket-writer.ts +80 -0
  260. package/src/utils/deps.ts +127 -0
  261. package/src/utils/fuzzy-match.ts +125 -0
  262. package/src/utils/logger.ts +127 -0
  263. package/src/utils/markdown.ts +254 -0
  264. package/src/utils/onboarding.ts +229 -0
  265. package/src/utils/prompts.ts +114 -0
  266. package/src/utils/run-commands.ts +112 -0
  267. package/src/utils/run-scripts.ts +142 -0
  268. package/src/utils/sanitize.ts +98 -0
  269. package/src/utils/secrets.ts +122 -0
  270. package/src/utils/shell-escape.ts +40 -0
  271. package/src/utils/utf8.ts +79 -0
  272. package/src/utils/workspace-state.ts +47 -0
  273. package/src/web/README.md +73 -0
  274. package/src/web/bun.lock +575 -0
  275. package/src/web/eslint.config.js +23 -0
  276. package/src/web/index.html +16 -0
  277. package/src/web/package.json +37 -0
  278. package/src/web/public/vite.svg +1 -0
  279. package/src/web/src/App.tsx +604 -0
  280. package/src/web/src/assets/react.svg +1 -0
  281. package/src/web/src/components/Terminal.tsx +207 -0
  282. package/src/web/src/hooks/useRelayConnection.ts +224 -0
  283. package/src/web/src/hooks/useTerminal.ts +699 -0
  284. package/src/web/src/index.css +55 -0
  285. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
  286. package/src/web/src/lib/crypto/frames.ts +205 -0
  287. package/src/web/src/lib/crypto/handshake.ts +396 -0
  288. package/src/web/src/lib/crypto/identity.ts +128 -0
  289. package/src/web/src/lib/crypto/keyexchange.ts +246 -0
  290. package/src/web/src/lib/crypto/relay-signing.ts +53 -0
  291. package/src/web/src/lib/invite.ts +58 -0
  292. package/src/web/src/lib/storage/identity-store.ts +94 -0
  293. package/src/web/src/main.tsx +10 -0
  294. package/src/web/src/types/identity.ts +45 -0
  295. package/src/web/tsconfig.app.json +28 -0
  296. package/src/web/tsconfig.json +7 -0
  297. package/src/web/tsconfig.node.json +26 -0
  298. package/src/web/vite.config.ts +31 -0
  299. package/todo-security.md +92 -0
  300. package/tsconfig.json +23 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  305. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  306. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  307. package/worker/bun.lock +237 -0
  308. package/worker/package.json +22 -0
  309. package/worker/schema.sql +96 -0
  310. package/worker/src/handlers/auth.ts +451 -0
  311. package/worker/src/handlers/subdomains.ts +376 -0
  312. package/worker/src/handlers/user.ts +98 -0
  313. package/worker/src/index.ts +70 -0
  314. package/worker/src/middleware/auth.ts +152 -0
  315. package/worker/src/services/cloudflare.ts +609 -0
  316. package/worker/src/types.ts +96 -0
  317. package/worker/tsconfig.json +15 -0
  318. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * RemoteMachineProvider - Remote Machine Access via Relay
3
+ *
4
+ * Implements MachineProvider interface for accessing remote machines
5
+ * through the relay server with encrypted communication.
6
+ */
7
+
8
+ import type {
9
+ MachineProvider,
10
+ CreateSessionOptions,
11
+ AttachSessionOptions,
12
+ } from './MachineProvider.js';
13
+ import type {
14
+ MachineInfo,
15
+ Project,
16
+ Workspace,
17
+ InboxItem,
18
+ SessionStream,
19
+ } from '../types.js';
20
+
21
+ // Use dynamic import for WebSocket to support both Node and browser
22
+ // In Node, we use the 'ws' package; in browser, we use native WebSocket
23
+
24
+ /**
25
+ * Common WebSocket interface for both Node.js (ws package) and browser environments
26
+ */
27
+ interface WebSocketLike {
28
+ send(data: string): void;
29
+ close(): void;
30
+ on(event: string, handler: (...args: unknown[]) => void): void;
31
+ off(event: string, handler: (...args: unknown[]) => void): void;
32
+ readyState: number;
33
+ }
34
+
35
+ /**
36
+ * Adapt the 'ws' package WebSocket to our common interface.
37
+ * The 'ws' package uses EventEmitter-style on/off which matches our interface.
38
+ */
39
+ function adaptWebSocket(ws: {
40
+ send(data: string): void;
41
+ close(): void;
42
+ on(event: string, handler: (...args: unknown[]) => void): void;
43
+ off(event: string, handler: (...args: unknown[]) => void): void;
44
+ readyState: number;
45
+ }): WebSocketLike {
46
+ return ws;
47
+ }
48
+
49
+ const OPEN = 1;
50
+
51
+ // ============================================================================
52
+ // Types
53
+ // ============================================================================
54
+
55
+ export interface RemoteMachineProviderConfig {
56
+ relayUrl: string;
57
+ /** Optional identity for the TUI client (generated if not provided) */
58
+ clientIdentityId?: string;
59
+ /** Machine ID to connect to (selected from list) */
60
+ machineId?: string;
61
+ }
62
+
63
+ interface RelayMessage {
64
+ type: string;
65
+ [key: string]: unknown;
66
+ }
67
+
68
+ // ============================================================================
69
+ // Provider Implementation
70
+ // ============================================================================
71
+
72
+ export class RemoteMachineProvider implements MachineProvider {
73
+ private ws: WebSocketLike | null = null;
74
+ private config: RemoteMachineProviderConfig;
75
+ private messageHandlers = new Map<string, (msg: RelayMessage) => void>();
76
+ private messageQueue: RelayMessage[] = [];
77
+ private connected = false;
78
+ private machineId: string | null = null;
79
+
80
+ constructor(config: RemoteMachineProviderConfig) {
81
+ this.config = config;
82
+ this.machineId = config.machineId ?? null;
83
+ }
84
+
85
+ /**
86
+ * Connect to relay server
87
+ */
88
+ async connect(): Promise<void> {
89
+ // Dynamic import for ws module (Node.js)
90
+ const { default: WebSocket } = await import('ws');
91
+
92
+ return new Promise((resolve, reject) => {
93
+ const url = new URL(this.config.relayUrl);
94
+ url.searchParams.set('role', 'client');
95
+ // Note: Authentication is now via challenge-response, not URL tokens
96
+ if (this.config.clientIdentityId) {
97
+ url.searchParams.set('clientId', this.config.clientIdentityId);
98
+ }
99
+
100
+ const ws = adaptWebSocket(new WebSocket(url.toString()));
101
+ this.ws = ws;
102
+
103
+ ws.on('open', () => {
104
+ this.connected = true;
105
+ // Send any queued messages
106
+ for (const msg of this.messageQueue) {
107
+ this.send(msg);
108
+ }
109
+ this.messageQueue = [];
110
+ resolve();
111
+ });
112
+
113
+ ws.on('message', (data: unknown) => {
114
+ try {
115
+ const dataStr = typeof data === 'string' ? data : String(data);
116
+ const msg = JSON.parse(dataStr) as RelayMessage;
117
+ this.handleMessage(msg);
118
+ } catch (e) {
119
+ console.error('Failed to parse relay message:', e);
120
+ }
121
+ });
122
+
123
+ ws.on('error', (err: unknown) => {
124
+ reject(err);
125
+ });
126
+
127
+ ws.on('close', () => {
128
+ this.connected = false;
129
+ this.ws = null;
130
+ });
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Send message to relay
136
+ */
137
+ private send(msg: RelayMessage): void {
138
+ if (!this.connected || !this.ws) {
139
+ this.messageQueue.push(msg);
140
+ return;
141
+ }
142
+ this.ws.send(JSON.stringify(msg));
143
+ }
144
+
145
+ /**
146
+ * Handle incoming relay message
147
+ */
148
+ private handleMessage(msg: RelayMessage): void {
149
+ const handler = this.messageHandlers.get(msg.type);
150
+ if (handler) {
151
+ handler(msg);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Send request and wait for response
157
+ */
158
+ private async request<T>(type: string, payload: Record<string, unknown> = {}): Promise<T> {
159
+ return new Promise((resolve, reject) => {
160
+ const responseType = `${type}_response`;
161
+ const errorType = `${type}_error`;
162
+
163
+ const cleanup = () => {
164
+ this.messageHandlers.delete(responseType);
165
+ this.messageHandlers.delete(errorType);
166
+ };
167
+
168
+ this.messageHandlers.set(responseType, (msg) => {
169
+ cleanup();
170
+ resolve(msg as T);
171
+ });
172
+
173
+ this.messageHandlers.set(errorType, (msg) => {
174
+ cleanup();
175
+ reject(new Error((msg.message as string) || 'Request failed'));
176
+ });
177
+
178
+ // Also handle generic error
179
+ this.messageHandlers.set('error', (msg) => {
180
+ cleanup();
181
+ reject(new Error((msg.message as string) || 'Request failed'));
182
+ });
183
+
184
+ this.send({
185
+ type,
186
+ machineId: this.machineId,
187
+ ...payload,
188
+ });
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Set the target machine ID
194
+ */
195
+ setMachineId(machineId: string): void {
196
+ this.machineId = machineId;
197
+ }
198
+
199
+ // ============================================================================
200
+ // MachineProvider Interface
201
+ // ============================================================================
202
+
203
+ async getMachineInfo(): Promise<MachineInfo> {
204
+ if (!this.machineId) {
205
+ throw new Error('No machine selected');
206
+ }
207
+
208
+ const response = await this.request<{ machine: MachineInfo }>('get_machine_info', {
209
+ machineId: this.machineId,
210
+ });
211
+
212
+ return response.machine;
213
+ }
214
+
215
+ async listProjects(): Promise<Project[]> {
216
+ if (!this.machineId) {
217
+ throw new Error('No machine selected');
218
+ }
219
+
220
+ const response = await this.request<{ projects: Project[] }>('list_projects');
221
+ return response.projects;
222
+ }
223
+
224
+ async listWorkspaces(projectName: string): Promise<Workspace[]> {
225
+ if (!this.machineId) {
226
+ throw new Error('No machine selected');
227
+ }
228
+
229
+ const response = await this.request<{ workspaces: Workspace[] }>('list_workspaces', {
230
+ projectName,
231
+ });
232
+ return response.workspaces;
233
+ }
234
+
235
+ async createSession(
236
+ projectName: string,
237
+ workspaceName: string,
238
+ options?: CreateSessionOptions
239
+ ): Promise<string> {
240
+ if (!this.machineId) {
241
+ throw new Error('No machine selected');
242
+ }
243
+
244
+ const response = await this.request<{ sessionId: string }>('create_session', {
245
+ projectName,
246
+ workspaceName,
247
+ sessionName: options?.sessionName,
248
+ shell: options?.shell,
249
+ });
250
+ return response.sessionId;
251
+ }
252
+
253
+ async attachSession(
254
+ sessionId: string,
255
+ options: AttachSessionOptions
256
+ ): Promise<SessionStream> {
257
+ if (!this.machineId) {
258
+ throw new Error('No machine selected');
259
+ }
260
+
261
+ // Create a session stream that communicates over the relay
262
+ const stream = new RemoteSessionStream(
263
+ this.ws!,
264
+ sessionId,
265
+ this.machineId,
266
+ options
267
+ );
268
+
269
+ await stream.attach();
270
+ return stream;
271
+ }
272
+
273
+ async detachSession(sessionId: string): Promise<void> {
274
+ if (!this.machineId) {
275
+ throw new Error('No machine selected');
276
+ }
277
+
278
+ await this.request('detach_session', { sessionId });
279
+ }
280
+
281
+ async getInbox(): Promise<InboxItem[]> {
282
+ if (!this.machineId) {
283
+ throw new Error('No machine selected');
284
+ }
285
+
286
+ const response = await this.request<{ items: InboxItem[] }>('get_inbox');
287
+ return response.items;
288
+ }
289
+
290
+ async markInboxRead(itemId: string): Promise<void> {
291
+ if (!this.machineId) {
292
+ throw new Error('No machine selected');
293
+ }
294
+
295
+ await this.request('mark_inbox_read', { itemId });
296
+ }
297
+
298
+ async clearInbox(): Promise<void> {
299
+ if (!this.machineId) {
300
+ throw new Error('No machine selected');
301
+ }
302
+
303
+ await this.request('clear_inbox');
304
+ }
305
+
306
+ dispose(): void {
307
+ if (this.ws) {
308
+ this.ws.close();
309
+ this.ws = null;
310
+ }
311
+ this.connected = false;
312
+ this.messageHandlers.clear();
313
+ }
314
+ }
315
+
316
+ // ============================================================================
317
+ // Remote Session Stream
318
+ // ============================================================================
319
+
320
+ class RemoteSessionStream implements SessionStream {
321
+ private ws: WebSocketLike;
322
+ private sessionId: string;
323
+ private machineId: string;
324
+ private options: AttachSessionOptions;
325
+ private dataHandler: ((data: Uint8Array) => void) | null = null;
326
+ private closeHandler: ((exitCode?: number) => void) | null = null;
327
+ private messageListener: ((data: unknown) => void) | null = null;
328
+
329
+ constructor(
330
+ ws: WebSocketLike,
331
+ sessionId: string,
332
+ machineId: string,
333
+ options: AttachSessionOptions
334
+ ) {
335
+ this.ws = ws;
336
+ this.sessionId = sessionId;
337
+ this.machineId = machineId;
338
+ this.options = options;
339
+ }
340
+
341
+ async attach(): Promise<void> {
342
+ // Set up message listener for session data
343
+ this.messageListener = (data: unknown) => {
344
+ try {
345
+ const dataStr = typeof data === 'string' ? data : String(data);
346
+ const msg = JSON.parse(dataStr);
347
+ if (msg.type === 'session_data' && msg.sessionId === this.sessionId) {
348
+ if (this.dataHandler && msg.data) {
349
+ const decoded = Buffer.from(msg.data, 'base64');
350
+ this.dataHandler(new Uint8Array(decoded));
351
+ }
352
+ } else if (msg.type === 'session_closed' && msg.sessionId === this.sessionId) {
353
+ if (this.closeHandler) {
354
+ this.closeHandler(msg.exitCode);
355
+ }
356
+ }
357
+ } catch {
358
+ // Ignore parse errors
359
+ }
360
+ };
361
+
362
+ this.ws.on('message', this.messageListener);
363
+
364
+ // Send attach request
365
+ this.ws.send(JSON.stringify({
366
+ type: 'attach_session',
367
+ machineId: this.machineId,
368
+ sessionId: this.sessionId,
369
+ cols: this.options.cols,
370
+ rows: this.options.rows,
371
+ force: this.options.force ?? false,
372
+ }));
373
+ }
374
+
375
+ write(data: Uint8Array): void {
376
+ const encoded = Buffer.from(data).toString('base64');
377
+ this.ws.send(JSON.stringify({
378
+ type: 'session_input',
379
+ machineId: this.machineId,
380
+ sessionId: this.sessionId,
381
+ data: encoded,
382
+ }));
383
+ }
384
+
385
+ resize(cols: number, rows: number): void {
386
+ this.ws.send(JSON.stringify({
387
+ type: 'session_resize',
388
+ machineId: this.machineId,
389
+ sessionId: this.sessionId,
390
+ cols,
391
+ rows,
392
+ }));
393
+ }
394
+
395
+ detach(): void {
396
+ this.ws.send(JSON.stringify({
397
+ type: 'detach_session',
398
+ machineId: this.machineId,
399
+ sessionId: this.sessionId,
400
+ }));
401
+ this.cleanup();
402
+ }
403
+
404
+ close(): void {
405
+ this.ws.send(JSON.stringify({
406
+ type: 'close_session',
407
+ machineId: this.machineId,
408
+ sessionId: this.sessionId,
409
+ }));
410
+ this.cleanup();
411
+ }
412
+
413
+ onData(handler: (data: Uint8Array) => void): void {
414
+ this.dataHandler = handler;
415
+ }
416
+
417
+ onClose(handler: (exitCode?: number) => void): void {
418
+ this.closeHandler = handler;
419
+ }
420
+
421
+ private cleanup(): void {
422
+ if (this.messageListener) {
423
+ this.ws.off('message', this.messageListener);
424
+ this.messageListener = null;
425
+ }
426
+ this.dataHandler = null;
427
+ this.closeHandler = null;
428
+ }
429
+ }
430
+
431
+ // ============================================================================
432
+ // Factory Function
433
+ // ============================================================================
434
+
435
+ /**
436
+ * Create a RemoteMachineProvider connected to the relay
437
+ */
438
+ export async function createRemoteMachineProvider(
439
+ config: RemoteMachineProviderConfig
440
+ ): Promise<RemoteMachineProvider> {
441
+ const provider = new RemoteMachineProvider(config);
442
+ await provider.connect();
443
+ return provider;
444
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Provider exports
3
+ */
4
+
5
+ export type {
6
+ MachineProvider,
7
+ CreateSessionOptions,
8
+ AttachSessionOptions,
9
+ MachineProviderEvent,
10
+ MachineProviderEventHandler,
11
+ EventedMachineProvider,
12
+ } from './MachineProvider.js';
13
+
14
+ export {
15
+ LocalMachineProvider,
16
+ getLocalMachineProvider,
17
+ } from './LocalMachineProvider.js';
18
+
19
+ export type {
20
+ RemoteMachineProviderConfig,
21
+ } from './RemoteMachineProvider.js';
22
+
23
+ export {
24
+ RemoteMachineProvider,
25
+ createRemoteMachineProvider,
26
+ } from './RemoteMachineProvider.js';
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Shared types for TUI and Web components
3
+ *
4
+ * These types are platform-agnostic and used by both interfaces.
5
+ */
6
+
7
+ // ============================================================================
8
+ // Machine Types
9
+ // ============================================================================
10
+
11
+ /** Machine connection status */
12
+ export type MachineStatus = 'connected' | 'connecting' | 'disconnected' | 'error';
13
+
14
+ /** Machine info for display */
15
+ export interface MachineInfo {
16
+ /** Unique machine ID */
17
+ id: string;
18
+ /** Display label */
19
+ label: string;
20
+ /** Whether this is the local machine */
21
+ isLocal: boolean;
22
+ /** Connection status */
23
+ status: MachineStatus;
24
+ /** Error message if status is 'error' */
25
+ error?: string;
26
+ }
27
+
28
+ // ============================================================================
29
+ // Project Types
30
+ // ============================================================================
31
+
32
+ /** Project state for display */
33
+ export interface Project {
34
+ /** Project name */
35
+ name: string;
36
+ /** GitHub repository (e.g., "owner/repo") */
37
+ repository: string;
38
+ /** Number of workspaces */
39
+ workspaceCount: number;
40
+ /** Whether this is the currently selected project */
41
+ isCurrent: boolean;
42
+ }
43
+
44
+ // ============================================================================
45
+ // Workspace Types
46
+ // ============================================================================
47
+
48
+ /** Session within a workspace */
49
+ export interface WorkspaceSession {
50
+ /** Session ID */
51
+ id: string;
52
+ /** Session name */
53
+ name: string;
54
+ /** Whether a client is attached */
55
+ attached: boolean;
56
+ /** Creation timestamp */
57
+ createdAt: number;
58
+ /** Current process title */
59
+ processTitle?: string;
60
+ }
61
+
62
+ /** Workspace state for display */
63
+ export interface Workspace {
64
+ /** Workspace name */
65
+ name: string;
66
+ /** Full path to workspace directory */
67
+ path: string;
68
+ /** Git branch name */
69
+ branch: string;
70
+ /** Commits ahead of remote */
71
+ ahead: number;
72
+ /** Commits behind remote */
73
+ behind: number;
74
+ /** Number of uncommitted changes */
75
+ uncommittedChanges: number;
76
+ /** Last commit date */
77
+ lastCommitDate: Date;
78
+ /** Whether workspace is stale (no recent activity) */
79
+ isStale: boolean;
80
+ /** Active sessions in this workspace */
81
+ sessions: WorkspaceSession[];
82
+ }
83
+
84
+ // ============================================================================
85
+ // Inbox Types
86
+ // ============================================================================
87
+
88
+ /** Inbox notification types */
89
+ export type InboxItemType = 'bell' | 'exit' | 'title' | 'idle';
90
+
91
+ /** Inbox notification item */
92
+ export interface InboxItem {
93
+ /** Unique ID */
94
+ id: string;
95
+ /** Session ID that generated the notification */
96
+ sessionId: string;
97
+ /** Session name */
98
+ sessionName: string;
99
+ /** Notification type */
100
+ type: InboxItemType;
101
+ /** Timestamp */
102
+ timestamp: number;
103
+ /** Whether the item has been read */
104
+ read: boolean;
105
+ /** Context/message content */
106
+ context: string;
107
+ /** Process title when notification occurred */
108
+ processTitle?: string;
109
+ /** Exit code (for exit type) */
110
+ exitCode?: number;
111
+ }
112
+
113
+ // ============================================================================
114
+ // Session Stream Types
115
+ // ============================================================================
116
+
117
+ /** Stream for terminal I/O */
118
+ export interface SessionStream {
119
+ /** Send data to the session */
120
+ write(data: Uint8Array): void;
121
+ /** Resize the terminal */
122
+ resize(cols: number, rows: number): void;
123
+ /** Detach from the session */
124
+ detach(): void;
125
+ /** Close the stream */
126
+ close(): void;
127
+ /** Register data handler */
128
+ onData(handler: (data: Uint8Array) => void): void;
129
+ /** Register close handler */
130
+ onClose(handler: (exitCode?: number) => void): void;
131
+ }
132
+
133
+ // ============================================================================
134
+ // Navigation Types
135
+ // ============================================================================
136
+
137
+ /** Navigation location */
138
+ export type NavigationLocation =
139
+ | { screen: 'machines' }
140
+ | { screen: 'projects'; machineId: string }
141
+ | { screen: 'workspaces'; machineId: string; projectName: string }
142
+ | { screen: 'session'; machineId: string; projectName: string; workspaceName: string; sessionId: string };
143
+
144
+ /** Panel focus */
145
+ export type PanelFocus = 'machines' | 'projects' | 'workspaces' | 'inbox';