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,199 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Client - connects to router, attaches to session
4
+ *
5
+ * Usage: bun client.ts [project] [workspace]
6
+ */
7
+
8
+ import { select } from "@inquirer/prompts";
9
+ import {
10
+ ROUTER_SOCKET,
11
+ type RouterCommand,
12
+ type RouterResponse,
13
+ type SessionInfo,
14
+ type AttachMode,
15
+ encodeControl,
16
+ isControl,
17
+ decodeControl,
18
+ type SessionEvent,
19
+ } from "./protocol";
20
+
21
+ const [project = "test", workspace = "default"] = process.argv.slice(2);
22
+
23
+ // Connect to router
24
+ async function routerCommand(cmd: RouterCommand): Promise<RouterResponse> {
25
+ return new Promise(async (resolve, reject) => {
26
+ const socket = await Bun.connect({
27
+ unix: ROUTER_SOCKET,
28
+ socket: {
29
+ data(socket, data) {
30
+ resolve(JSON.parse(data.toString()));
31
+ socket.end();
32
+ },
33
+ error(socket, error) {
34
+ reject(error);
35
+ },
36
+ connectError(socket, error) {
37
+ reject(new Error("Router not running. Start with: bun router.ts"));
38
+ }
39
+ }
40
+ });
41
+
42
+ socket.write(JSON.stringify(cmd));
43
+ });
44
+ }
45
+
46
+ // Get or create session
47
+ console.log(`Connecting to ${project}/${workspace}...`);
48
+
49
+ let session: SessionInfo;
50
+
51
+ try {
52
+ let response = await routerCommand({
53
+ type: "create",
54
+ project,
55
+ workspace,
56
+ cwd: process.cwd()
57
+ });
58
+
59
+ // Handle already-attached case
60
+ if (response.type === "already-attached") {
61
+ console.log(`\nSession "${project}/${workspace}" is already attached.\n`);
62
+
63
+ const choice = await select({
64
+ message: "What would you like to do?",
65
+ choices: [
66
+ { value: "take-over", name: "Take over (disconnect other client)" },
67
+ { value: "new", name: "Create new session for this workspace" },
68
+ { value: "cancel", name: "Cancel" },
69
+ ]
70
+ }) as AttachMode;
71
+
72
+ if (choice === "cancel") {
73
+ console.log("Cancelled.");
74
+ process.exit(0);
75
+ }
76
+
77
+ if (choice === "take-over") {
78
+ // Attach with take-over mode
79
+ response = await routerCommand({
80
+ type: "attach",
81
+ sessionId: response.session.id,
82
+ mode: "take-over"
83
+ });
84
+ } else if (choice === "new") {
85
+ // Force create new session (kill old one first, then create)
86
+ await routerCommand({ type: "kill", sessionId: response.session.id });
87
+ response = await routerCommand({
88
+ type: "create",
89
+ project,
90
+ workspace,
91
+ cwd: process.cwd()
92
+ });
93
+ }
94
+ }
95
+
96
+ if (response.type === "error") {
97
+ console.error("Error:", response.message);
98
+ process.exit(1);
99
+ }
100
+
101
+ if (response.type !== "created") {
102
+ console.error("Unexpected response:", response);
103
+ process.exit(1);
104
+ }
105
+
106
+ session = response.session;
107
+ } catch (e: any) {
108
+ console.error(e.message);
109
+ process.exit(1);
110
+ }
111
+
112
+ console.log(`Attached to session ${session.id}`);
113
+ console.log("Press Ctrl+D to detach\n");
114
+
115
+ // Connect to session
116
+ const sessionSocket = await Bun.connect({
117
+ unix: session.socketPath,
118
+ socket: {
119
+ data(socket, data) {
120
+ const buf = Buffer.from(data);
121
+
122
+ if (isControl(buf)) {
123
+ const event = decodeControl(buf) as SessionEvent;
124
+
125
+ switch (event.type) {
126
+ case "attached":
127
+ // Replay scrollback
128
+ if (event.scrollback) {
129
+ const scrollback = Buffer.from(event.scrollback, "base64");
130
+ process.stdout.write(scrollback);
131
+ }
132
+ break;
133
+
134
+ case "exited":
135
+ process.stdin.setRawMode(false);
136
+ console.log(`\nSession exited with code ${event.code}`);
137
+ process.exit(event.code);
138
+ break;
139
+
140
+ case "kicked":
141
+ process.stdin.setRawMode(false);
142
+ console.log("\n\nAnother client took over this session.");
143
+ process.exit(0);
144
+ break;
145
+
146
+ case "pong":
147
+ break;
148
+ }
149
+ } else {
150
+ // PTY output - write to terminal
151
+ process.stdout.write(buf);
152
+ }
153
+ },
154
+
155
+ close() {
156
+ process.stdin.setRawMode(false);
157
+ console.log("\nDisconnected from session");
158
+ process.exit(0);
159
+ },
160
+
161
+ error(socket, error) {
162
+ process.stdin.setRawMode(false);
163
+ console.error("\nSession error:", error.message);
164
+ process.exit(1);
165
+ }
166
+ }
167
+ });
168
+
169
+ // Send initial resize
170
+ sessionSocket.write(encodeControl({
171
+ type: "resize",
172
+ cols: process.stdout.columns || 80,
173
+ rows: process.stdout.rows || 24
174
+ }));
175
+
176
+ // Handle terminal resize
177
+ process.stdout.on("resize", () => {
178
+ sessionSocket.write(encodeControl({
179
+ type: "resize",
180
+ cols: process.stdout.columns,
181
+ rows: process.stdout.rows
182
+ }));
183
+ });
184
+
185
+ // Forward stdin to session
186
+ process.stdin.setRawMode(true);
187
+ process.stdin.resume();
188
+
189
+ for await (const chunk of process.stdin) {
190
+ // Check for Ctrl+D (detach)
191
+ if (chunk[0] === 4) {
192
+ sessionSocket.write(encodeControl({ type: "detach" }));
193
+ process.stdin.setRawMode(false);
194
+ console.log("\nDetached (session still running)");
195
+ process.exit(0);
196
+ }
197
+
198
+ sessionSocket.write(chunk);
199
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Shared protocol between router, sessions, and clients
3
+ */
4
+
5
+ export const ROUTER_SOCKET = "/tmp/spaces-router.sock";
6
+ export const SESSION_SOCKET_PREFIX = "/tmp/spaces-session-";
7
+
8
+ // Router commands (JSON over socket)
9
+ export type RouterCommand =
10
+ | { type: "list" }
11
+ | { type: "create"; project: string; workspace: string; cwd: string }
12
+ | { type: "attach"; sessionId: string; mode?: AttachMode }
13
+ | { type: "kill"; sessionId: string }
14
+ | { type: "kick"; sessionId: string }; // Disconnect current client
15
+
16
+ export type RouterResponse =
17
+ | { type: "sessions"; sessions: SessionInfo[] }
18
+ | { type: "created"; session: SessionInfo }
19
+ | { type: "already-attached"; session: SessionInfo } // Session exists but has a client
20
+ | { type: "error"; message: string }
21
+ | { type: "ok" };
22
+
23
+ export interface SessionInfo {
24
+ id: string;
25
+ project: string;
26
+ workspace: string;
27
+ socketPath: string;
28
+ pid: number;
29
+ attached: boolean; // true if a client is connected
30
+ createdAt: number;
31
+ }
32
+
33
+ // When attaching to an already-attached session
34
+ export type AttachMode =
35
+ | "take-over" // Disconnect existing client, you take over
36
+ | "new" // Create a new session for the same workspace
37
+ | "cancel"; // Abort
38
+
39
+ // Session protocol (binary + JSON control)
40
+ // Control messages start with 0x00, data is raw bytes
41
+ export const CONTROL_PREFIX = 0x00;
42
+
43
+ export type SessionControl =
44
+ | { type: "resize"; cols: number; rows: number }
45
+ | { type: "detach" }
46
+ | { type: "ping" };
47
+
48
+ export type SessionEvent =
49
+ | { type: "attached"; scrollback: string }
50
+ | { type: "exited"; code: number }
51
+ | { type: "kicked" } // Another client took over
52
+ | { type: "pong" };
53
+
54
+ // Helper to encode control message
55
+ export function encodeControl(msg: SessionControl | SessionEvent): Buffer {
56
+ const json = JSON.stringify(msg);
57
+ const buf = Buffer.alloc(1 + 4 + json.length);
58
+ buf[0] = CONTROL_PREFIX;
59
+ buf.writeUInt32BE(json.length, 1);
60
+ buf.write(json, 5);
61
+ return buf;
62
+ }
63
+
64
+ // Helper to check if data is control message
65
+ export function isControl(data: Buffer): boolean {
66
+ return data[0] === CONTROL_PREFIX;
67
+ }
68
+
69
+ // Helper to decode control message
70
+ export function decodeControl(data: Buffer): SessionControl | SessionEvent {
71
+ const len = data.readUInt32BE(1);
72
+ const json = data.subarray(5, 5 + len).toString();
73
+ return JSON.parse(json);
74
+ }
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Router - manages session lifecycle
4
+ * Always running, spawns sessions on demand
5
+ *
6
+ * Usage: bun router.ts
7
+ */
8
+
9
+ import { unlinkSync, existsSync } from "fs";
10
+ import { spawn } from "bun";
11
+ import {
12
+ ROUTER_SOCKET,
13
+ SESSION_SOCKET_PREFIX,
14
+ type RouterCommand,
15
+ type RouterResponse,
16
+ type SessionInfo,
17
+ } from "./protocol";
18
+
19
+ // Clean up existing socket
20
+ try { unlinkSync(ROUTER_SOCKET); } catch {}
21
+
22
+ // Active sessions
23
+ const sessions = new Map<string, {
24
+ info: SessionInfo;
25
+ proc: Bun.Subprocess;
26
+ stdin: WritableStream<Uint8Array>;
27
+ }>();
28
+
29
+ // Generate session ID
30
+ function genId(): string {
31
+ return Math.random().toString(36).substring(2, 10);
32
+ }
33
+
34
+ // Find session by project/workspace
35
+ function findSession(project: string, workspace: string) {
36
+ for (const [id, session] of sessions) {
37
+ if (session.info.project === project && session.info.workspace === workspace) {
38
+ return session;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ // Spawn a new session
45
+ async function createSession(project: string, workspace: string, cwd: string): Promise<SessionInfo> {
46
+ const id = genId();
47
+ const socketPath = `${SESSION_SOCKET_PREFIX}${id}.sock`;
48
+
49
+ const proc = spawn({
50
+ cmd: ["bun", "run", `${import.meta.dir}/session.ts`, socketPath, cwd, project, workspace],
51
+ stdout: "pipe",
52
+ stdin: "pipe",
53
+ stderr: "inherit",
54
+ });
55
+
56
+ // Wait for ready event
57
+ const reader = proc.stdout.getReader();
58
+ const { value } = await reader.read();
59
+ const ready = JSON.parse(new TextDecoder().decode(value));
60
+
61
+ if (ready.event !== "ready") {
62
+ throw new Error("Session failed to start");
63
+ }
64
+
65
+ const info: SessionInfo = {
66
+ id,
67
+ project,
68
+ workspace,
69
+ socketPath,
70
+ pid: ready.pid,
71
+ attached: false,
72
+ createdAt: Date.now(),
73
+ };
74
+
75
+ const sessionData = { info, proc, stdin: proc.stdin };
76
+ sessions.set(id, sessionData);
77
+
78
+ // Monitor session stdout for state updates
79
+ (async () => {
80
+ while (true) {
81
+ const { done, value } = await reader.read();
82
+ if (done) break;
83
+
84
+ try {
85
+ const lines = new TextDecoder().decode(value).trim().split('\n');
86
+ for (const line of lines) {
87
+ const event = JSON.parse(line);
88
+ if (event.attached !== undefined) {
89
+ const session = sessions.get(id);
90
+ if (session) session.info.attached = event.attached;
91
+ }
92
+ }
93
+ } catch {}
94
+ }
95
+
96
+ // Session ended
97
+ sessions.delete(id);
98
+ console.log(`[router] Session ${id} ended`);
99
+ })();
100
+
101
+ console.log(`[router] Created session ${id} for ${project}/${workspace}`);
102
+ return info;
103
+ }
104
+
105
+ // Kick client from session
106
+ async function kickSession(sessionId: string): Promise<boolean> {
107
+ const session = sessions.get(sessionId);
108
+ if (!session) return false;
109
+
110
+ const writer = session.stdin.getWriter();
111
+ await writer.write(new TextEncoder().encode("kick\n"));
112
+ writer.releaseLock();
113
+ return true;
114
+ }
115
+
116
+ // Start router server
117
+ const server = Bun.listen({
118
+ unix: ROUTER_SOCKET,
119
+ socket: {
120
+ async data(socket, data) {
121
+ try {
122
+ const cmd: RouterCommand = JSON.parse(data.toString());
123
+ let response: RouterResponse;
124
+
125
+ switch (cmd.type) {
126
+ case "list":
127
+ response = {
128
+ type: "sessions",
129
+ sessions: Array.from(sessions.values()).map(s => s.info)
130
+ };
131
+ break;
132
+
133
+ case "create": {
134
+ // Check if session already exists for this workspace
135
+ const existing = findSession(cmd.project, cmd.workspace);
136
+
137
+ if (existing) {
138
+ if (existing.info.attached) {
139
+ // Session exists and has a client
140
+ response = { type: "already-attached", session: existing.info };
141
+ } else {
142
+ // Session exists but detached - reuse it
143
+ response = { type: "created", session: existing.info };
144
+ }
145
+ } else {
146
+ // Create new session
147
+ const session = await createSession(cmd.project, cmd.workspace, cmd.cwd);
148
+ response = { type: "created", session };
149
+ }
150
+ break;
151
+ }
152
+
153
+ case "attach": {
154
+ const session = sessions.get(cmd.sessionId);
155
+ if (!session) {
156
+ response = { type: "error", message: "Session not found" };
157
+ } else if (session.info.attached && cmd.mode !== "take-over") {
158
+ response = { type: "already-attached", session: session.info };
159
+ } else {
160
+ if (cmd.mode === "take-over" && session.info.attached) {
161
+ await kickSession(cmd.sessionId);
162
+ await Bun.sleep(50); // Let kick propagate
163
+ }
164
+ response = { type: "created", session: session.info };
165
+ }
166
+ break;
167
+ }
168
+
169
+ case "kick": {
170
+ const kicked = await kickSession(cmd.sessionId);
171
+ response = kicked ? { type: "ok" } : { type: "error", message: "Session not found" };
172
+ break;
173
+ }
174
+
175
+ case "kill": {
176
+ const toKill = sessions.get(cmd.sessionId);
177
+ if (toKill) {
178
+ toKill.proc.kill();
179
+ sessions.delete(cmd.sessionId);
180
+ response = { type: "ok" };
181
+ } else {
182
+ response = { type: "error", message: "Session not found" };
183
+ }
184
+ break;
185
+ }
186
+
187
+ default:
188
+ response = { type: "error", message: "Unknown command" };
189
+ }
190
+
191
+ socket.write(JSON.stringify(response));
192
+ } catch (e: any) {
193
+ socket.write(JSON.stringify({ type: "error", message: e.message }));
194
+ }
195
+ },
196
+ error(socket, error) {
197
+ console.error("[router] Client error:", error.message);
198
+ }
199
+ }
200
+ });
201
+
202
+ console.log(`[router] Listening on ${ROUTER_SOCKET}`);
203
+ console.log("[router] Ready");
204
+
205
+ // Cleanup on exit
206
+ process.on("SIGTERM", () => {
207
+ for (const [id, session] of sessions) {
208
+ session.proc.kill();
209
+ }
210
+ server.stop();
211
+ try { unlinkSync(ROUTER_SOCKET); } catch {}
212
+ process.exit(0);
213
+ });
214
+
215
+ process.on("SIGINT", () => {
216
+ process.emit("SIGTERM" as any);
217
+ });
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Session server - manages a single PTY session
4
+ * One client at a time, supports kick for takeover
5
+ *
6
+ * Usage: bun session.ts <socket-path> <cwd> <project> <workspace>
7
+ */
8
+
9
+ import { unlinkSync } from "fs";
10
+ import {
11
+ encodeControl,
12
+ isControl,
13
+ decodeControl,
14
+ type SessionControl,
15
+ } from "./protocol";
16
+
17
+ const [socketPath, cwd, project, workspace] = process.argv.slice(2);
18
+
19
+ if (!socketPath || !cwd) {
20
+ console.error("Usage: bun session.ts <socket-path> <cwd> <project> <workspace>");
21
+ process.exit(1);
22
+ }
23
+
24
+ // Clean up existing socket
25
+ try { unlinkSync(socketPath); } catch {}
26
+
27
+ // Scrollback buffer (keep last 50KB)
28
+ const MAX_SCROLLBACK = 50 * 1024;
29
+ let scrollback = Buffer.alloc(0);
30
+
31
+ // Current attached client (only one allowed)
32
+ let client: any = null;
33
+
34
+ // Create PTY
35
+ const terminal = new Bun.Terminal({
36
+ cols: 120,
37
+ rows: 40,
38
+ data(term, data) {
39
+ // Add to scrollback
40
+ scrollback = Buffer.concat([scrollback, data]);
41
+ if (scrollback.length > MAX_SCROLLBACK) {
42
+ scrollback = scrollback.subarray(-MAX_SCROLLBACK);
43
+ }
44
+
45
+ // Send to attached client
46
+ if (client) {
47
+ client.write(data);
48
+ }
49
+ }
50
+ });
51
+
52
+ // Spawn shell
53
+ const proc = Bun.spawn(["bash"], {
54
+ terminal,
55
+ cwd,
56
+ env: {
57
+ ...process.env,
58
+ SPACES_PROJECT: project,
59
+ SPACES_WORKSPACE: workspace,
60
+ },
61
+ });
62
+
63
+ // Handle shell exit
64
+ proc.exited.then(code => {
65
+ if (client) {
66
+ client.write(encodeControl({ type: "exited", code }));
67
+ }
68
+ // Give client time to receive exit message
69
+ setTimeout(() => {
70
+ server.stop();
71
+ try { unlinkSync(socketPath); } catch {}
72
+ process.exit(code);
73
+ }, 100);
74
+ });
75
+
76
+ // Report state to router
77
+ function reportState(event: string) {
78
+ console.log(JSON.stringify({ event, attached: client !== null }));
79
+ }
80
+
81
+ // Kick current client
82
+ function kickClient() {
83
+ if (client) {
84
+ client.write(encodeControl({ type: "kicked" }));
85
+ client.end();
86
+ client = null;
87
+ reportState("client_kicked");
88
+ }
89
+ }
90
+
91
+ // Start server
92
+ const server = Bun.listen({
93
+ unix: socketPath,
94
+ socket: {
95
+ open(socket) {
96
+ if (client) {
97
+ // Already have a client - kick them
98
+ kickClient();
99
+ }
100
+
101
+ client = socket;
102
+ reportState("client_attached");
103
+
104
+ // Send scrollback to new client
105
+ const attachMsg = encodeControl({
106
+ type: "attached",
107
+ scrollback: scrollback.toString("base64")
108
+ });
109
+ socket.write(attachMsg);
110
+ },
111
+
112
+ data(socket, data) {
113
+ const buf = Buffer.from(data);
114
+
115
+ if (isControl(buf)) {
116
+ const ctrl = decodeControl(buf) as SessionControl;
117
+
118
+ switch (ctrl.type) {
119
+ case "resize":
120
+ terminal.resize(ctrl.cols, ctrl.rows);
121
+ break;
122
+ case "detach":
123
+ if (socket === client) {
124
+ client = null;
125
+ reportState("client_detached");
126
+ }
127
+ socket.end();
128
+ break;
129
+ case "ping":
130
+ socket.write(encodeControl({ type: "pong" }));
131
+ break;
132
+ }
133
+ } else {
134
+ // Raw input - write to PTY
135
+ terminal.write(buf);
136
+ }
137
+ },
138
+
139
+ close(socket) {
140
+ if (socket === client) {
141
+ client = null;
142
+ reportState("client_disconnected");
143
+ }
144
+ },
145
+
146
+ error(socket, error) {
147
+ console.error(JSON.stringify({ event: "error", message: error.message }));
148
+ if (socket === client) {
149
+ client = null;
150
+ }
151
+ }
152
+ }
153
+ });
154
+
155
+ console.log(JSON.stringify({
156
+ event: "ready",
157
+ socketPath,
158
+ pid: process.pid,
159
+ project,
160
+ workspace,
161
+ attached: false
162
+ }));
163
+
164
+ // Handle stdin commands from router (for kick)
165
+ const decoder = new TextDecoder();
166
+ for await (const chunk of Bun.stdin.stream()) {
167
+ const cmd = decoder.decode(chunk).trim();
168
+ if (cmd === "kick") {
169
+ kickClient();
170
+ }
171
+ }
172
+
173
+ // Handle termination
174
+ process.on("SIGTERM", () => {
175
+ proc.kill();
176
+ terminal.close();
177
+ server.stop();
178
+ try { unlinkSync(socketPath); } catch {}
179
+ process.exit(0);
180
+ });