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,425 @@
1
+ /**
2
+ * LocalMachineProvider
3
+ *
4
+ * MachineProvider implementation for the local machine.
5
+ * Wraps existing TUI state functions and tmux-lite CLI.
6
+ */
7
+
8
+ import { existsSync, readdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { hostname } from 'os';
11
+ import { createBufferedSocketWriter } from '../../utils/bun-socket-writer.js';
12
+ import {
13
+ getAllProjectNames,
14
+ readProjectConfig,
15
+ getCurrentProject,
16
+ getProjectWorkspacesDir,
17
+ } from '../../core/config.js';
18
+ import { getWorktreeInfo } from '../../core/git.js';
19
+ import {
20
+ listSessions as tmuxListSessions,
21
+ createSession as tmuxCreateSession,
22
+ getInbox as tmuxGetInbox,
23
+ markInboxRead as tmuxMarkInboxRead,
24
+ clearInbox as tmuxClearInbox,
25
+ ensureServer,
26
+ send,
27
+ type Session as TmuxSession,
28
+ } from '../../lib/tmux-lite/cli.js';
29
+ import type {
30
+ MachineProvider,
31
+ CreateSessionOptions,
32
+ AttachSessionOptions,
33
+ MachineProviderEvent,
34
+ MachineProviderEventHandler,
35
+ EventedMachineProvider,
36
+ } from './MachineProvider.js';
37
+ import type {
38
+ MachineInfo,
39
+ Project,
40
+ Workspace,
41
+ WorkspaceSession,
42
+ InboxItem,
43
+ SessionStream,
44
+ } from '../types.js';
45
+
46
+ const STALE_DAYS = 30;
47
+ const LOCAL_MACHINE_ID = 'local';
48
+
49
+ /**
50
+ * Convert tmux-lite session to shared WorkspaceSession type
51
+ */
52
+ function toWorkspaceSession(session: TmuxSession): WorkspaceSession {
53
+ return {
54
+ id: session.id,
55
+ name: session.name,
56
+ attached: session.attached,
57
+ createdAt: session.createdAt,
58
+ processTitle: session.processTitle,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * LocalMachineProvider - access local machine resources
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const provider = new LocalMachineProvider();
68
+ * const projects = await provider.listProjects();
69
+ * const workspaces = await provider.listWorkspaces('my-project');
70
+ * ```
71
+ */
72
+ export class LocalMachineProvider implements EventedMachineProvider {
73
+ private eventHandlers: Set<MachineProviderEventHandler> = new Set();
74
+ private disposed = false;
75
+
76
+ /**
77
+ * Get information about the local machine
78
+ */
79
+ async getMachineInfo(): Promise<MachineInfo> {
80
+ return {
81
+ id: LOCAL_MACHINE_ID,
82
+ label: hostname() || 'Local',
83
+ isLocal: true,
84
+ status: 'connected',
85
+ };
86
+ }
87
+
88
+ /**
89
+ * List all projects on the local machine
90
+ */
91
+ async listProjects(): Promise<Project[]> {
92
+ const projectNames = getAllProjectNames();
93
+ const currentProject = getCurrentProject();
94
+
95
+ return projectNames.map((name) => {
96
+ const config = readProjectConfig(name);
97
+ const workspacesDir = getProjectWorkspacesDir(name);
98
+ let workspaceCount = 0;
99
+
100
+ if (existsSync(workspacesDir)) {
101
+ workspaceCount = readdirSync(workspacesDir).filter((entry) => {
102
+ const path = join(workspacesDir, entry);
103
+ return existsSync(path) && readdirSync(path).length > 0;
104
+ }).length;
105
+ }
106
+
107
+ return {
108
+ name,
109
+ repository: config.repository,
110
+ workspaceCount,
111
+ isCurrent: name === currentProject,
112
+ };
113
+ });
114
+ }
115
+
116
+ /**
117
+ * List workspaces for a project
118
+ */
119
+ async listWorkspaces(projectName: string): Promise<Workspace[]> {
120
+ const workspacesDir = getProjectWorkspacesDir(projectName);
121
+
122
+ if (!existsSync(workspacesDir)) {
123
+ return [];
124
+ }
125
+
126
+ const workspaceNames = readdirSync(workspacesDir).filter((entry) => {
127
+ const path = join(workspacesDir, entry);
128
+ return existsSync(path) && readdirSync(path).length > 0;
129
+ });
130
+
131
+ // Get all tmux-lite sessions
132
+ let allSessions: TmuxSession[] = [];
133
+ try {
134
+ allSessions = await tmuxListSessions();
135
+ } catch {
136
+ // Server might not be running, that's fine
137
+ }
138
+
139
+ const workspaces: Workspace[] = [];
140
+ const now = new Date();
141
+
142
+ for (const name of workspaceNames) {
143
+ const workspacePath = join(workspacesDir, name);
144
+ const info = await getWorktreeInfo(workspacePath);
145
+
146
+ if (info) {
147
+ const daysSinceCommit = Math.floor(
148
+ (now.getTime() - info.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24)
149
+ );
150
+
151
+ // Find sessions for this workspace (name pattern: project:workspace:n)
152
+ const sessionPrefix = `${projectName}:${name}:`;
153
+ const workspaceSessions = allSessions
154
+ .filter(s => s.name.startsWith(sessionPrefix))
155
+ .map(toWorkspaceSession);
156
+
157
+ workspaces.push({
158
+ name: info.name,
159
+ path: info.path,
160
+ branch: info.branch,
161
+ ahead: info.ahead,
162
+ behind: info.behind,
163
+ uncommittedChanges: info.uncommittedChanges,
164
+ lastCommitDate: info.lastCommitDate,
165
+ isStale: daysSinceCommit > STALE_DAYS,
166
+ sessions: workspaceSessions,
167
+ });
168
+ }
169
+ }
170
+
171
+ return workspaces;
172
+ }
173
+
174
+ /**
175
+ * Create a new session in a workspace
176
+ */
177
+ async createSession(
178
+ projectName: string,
179
+ workspaceName: string,
180
+ options?: CreateSessionOptions
181
+ ): Promise<string> {
182
+ const workspacesDir = getProjectWorkspacesDir(projectName);
183
+ const workspacePath = join(workspacesDir, workspaceName);
184
+
185
+ // Count existing sessions to generate unique name
186
+ let allSessions: TmuxSession[] = [];
187
+ try {
188
+ allSessions = await tmuxListSessions();
189
+ } catch {
190
+ // Ignore
191
+ }
192
+
193
+ const sessionPrefix = `${projectName}:${workspaceName}:`;
194
+ const existingCount = allSessions.filter(s => s.name.startsWith(sessionPrefix)).length;
195
+ const sessionName = `${sessionPrefix}${existingCount + 1}`;
196
+
197
+ const cwd = options?.cwd ?? workspacePath;
198
+ const session = await tmuxCreateSession(sessionName, cwd);
199
+
200
+ return session.id;
201
+ }
202
+
203
+ /**
204
+ * Attach to an existing session
205
+ *
206
+ * Returns a SessionStream for terminal I/O.
207
+ * For local sessions, this connects directly to the tmux-lite socket.
208
+ */
209
+ async attachSession(
210
+ sessionId: string,
211
+ options: AttachSessionOptions
212
+ ): Promise<SessionStream> {
213
+ await ensureServer();
214
+
215
+ // Get session info
216
+ const sessions = await tmuxListSessions();
217
+ const session = sessions.find(s => s.id === sessionId);
218
+ if (!session) {
219
+ throw new Error(`Session not found: ${sessionId}`);
220
+ }
221
+
222
+ // Connect to session socket
223
+ const socketPath = session.socketPath;
224
+
225
+ return new Promise((resolve, reject) => {
226
+ let dataHandler: ((data: Uint8Array) => void) | null = null;
227
+ let closeHandler: ((exitCode?: number) => void) | null = null;
228
+ let socket: Awaited<ReturnType<typeof Bun.connect>> | null = null;
229
+ let socketWriter: ReturnType<typeof createBufferedSocketWriter> | null = null;
230
+ let buffer = Buffer.alloc(0);
231
+
232
+ const stream: SessionStream = {
233
+ write(data: Uint8Array) {
234
+ if (socket) {
235
+ const { encodePTY } = require('../../lib/tmux-lite/protocol.js');
236
+ const frame = encodePTY(Buffer.from(data));
237
+ if (socketWriter) socketWriter.write(frame);
238
+ else socket.write(frame);
239
+ }
240
+ },
241
+ resize(cols: number, rows: number) {
242
+ if (socket) {
243
+ const { encodeControl } = require('../../lib/tmux-lite/protocol.js');
244
+ const frame = encodeControl({ type: 'resize', cols, rows });
245
+ if (socketWriter) socketWriter.write(frame);
246
+ else socket.write(frame);
247
+ }
248
+ },
249
+ detach() {
250
+ if (socket) {
251
+ const { encodeControl } = require('../../lib/tmux-lite/protocol.js');
252
+ const frame = encodeControl({ type: 'detach' });
253
+ if (socketWriter) socketWriter.write(frame);
254
+ else socket.write(frame);
255
+ }
256
+ },
257
+ close() {
258
+ socket?.end();
259
+ socket = null;
260
+ socketWriter = null;
261
+ },
262
+ onData(handler: (data: Uint8Array) => void) {
263
+ dataHandler = handler;
264
+ },
265
+ onClose(handler: (exitCode?: number) => void) {
266
+ closeHandler = handler;
267
+ },
268
+ };
269
+
270
+ Bun.connect({
271
+ unix: socketPath,
272
+ socket: {
273
+ open(s) {
274
+ socket = s;
275
+ socketWriter = createBufferedSocketWriter(s);
276
+ // Send attach-init
277
+ const { encodeControl } = require('../../lib/tmux-lite/protocol.js');
278
+ socketWriter.write(encodeControl({
279
+ type: 'attach-init',
280
+ cols: options.cols,
281
+ rows: options.rows,
282
+ clientType: options.clientType,
283
+ }));
284
+ resolve(stream);
285
+ },
286
+ drain() {
287
+ socketWriter?.flush();
288
+ },
289
+ data(_, data) {
290
+ const { parseFrames, decodeControl, FrameType } = require('../../lib/tmux-lite/protocol.js');
291
+ buffer = Buffer.concat([buffer, Buffer.from(data)]);
292
+
293
+ let frames;
294
+ let remaining;
295
+ try {
296
+ const result = parseFrames(buffer);
297
+ frames = result.frames;
298
+ remaining = result.remaining;
299
+ } catch (err) {
300
+ // Protocol error - likely desync or corrupted data
301
+ const msg = err instanceof Error ? err.message : 'Frame parse error';
302
+ console.error(`[LocalMachineProvider] Frame parse error: ${msg}`);
303
+ closeHandler?.();
304
+ socket?.end();
305
+ socket = null;
306
+ return;
307
+ }
308
+ // Copy remaining bytes - subarray references can become invalid when Bun reuses buffers
309
+ buffer = Buffer.from(remaining);
310
+
311
+ for (const frame of frames) {
312
+ if (frame.type === FrameType.CONTROL) {
313
+ const event = decodeControl(frame.payload);
314
+ if (event.type === 'exited') {
315
+ closeHandler?.(event.code);
316
+ socket?.end();
317
+ socket = null;
318
+ } else if (event.type === 'kicked') {
319
+ closeHandler?.();
320
+ socket?.end();
321
+ socket = null;
322
+ }
323
+ } else if (frame.type === FrameType.PTY) {
324
+ dataHandler?.(frame.payload);
325
+ }
326
+ }
327
+ },
328
+ close() {
329
+ closeHandler?.();
330
+ socket = null;
331
+ },
332
+ error(_, e) {
333
+ reject(e);
334
+ },
335
+ connectError(_, e) {
336
+ reject(e);
337
+ },
338
+ },
339
+ }).catch(reject);
340
+ });
341
+ }
342
+
343
+ /**
344
+ * Detach from a session
345
+ */
346
+ async detachSession(sessionId: string): Promise<void> {
347
+ // Detach is handled by the SessionStream.detach() method
348
+ // This is a no-op for local sessions
349
+ }
350
+
351
+ /**
352
+ * Get inbox notifications
353
+ */
354
+ async getInbox(): Promise<InboxItem[]> {
355
+ try {
356
+ const items = await tmuxGetInbox();
357
+ return items.map(item => ({
358
+ id: item.id,
359
+ sessionId: item.sessionId,
360
+ sessionName: item.sessionName,
361
+ type: item.type,
362
+ timestamp: item.timestamp,
363
+ read: item.read,
364
+ context: item.context,
365
+ processTitle: item.processTitle,
366
+ exitCode: item.exitCode,
367
+ }));
368
+ } catch {
369
+ // Server might not be running
370
+ return [];
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Mark inbox item as read
376
+ */
377
+ async markInboxRead(itemId: string): Promise<void> {
378
+ await tmuxMarkInboxRead(itemId);
379
+ }
380
+
381
+ /**
382
+ * Clear all inbox items
383
+ */
384
+ async clearInbox(): Promise<void> {
385
+ await tmuxClearInbox();
386
+ }
387
+
388
+ /**
389
+ * Subscribe to provider events
390
+ */
391
+ onEvent(handler: MachineProviderEventHandler): () => void {
392
+ this.eventHandlers.add(handler);
393
+ return () => this.eventHandlers.delete(handler);
394
+ }
395
+
396
+ /**
397
+ * Emit an event to all handlers
398
+ */
399
+ private emit(event: MachineProviderEvent): void {
400
+ for (const handler of this.eventHandlers) {
401
+ handler(event);
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Dispose of the provider
407
+ */
408
+ dispose(): void {
409
+ if (this.disposed) return;
410
+ this.disposed = true;
411
+ this.eventHandlers.clear();
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Singleton instance for convenience
417
+ */
418
+ let localProvider: LocalMachineProvider | null = null;
419
+
420
+ export function getLocalMachineProvider(): LocalMachineProvider {
421
+ if (!localProvider) {
422
+ localProvider = new LocalMachineProvider();
423
+ }
424
+ return localProvider;
425
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * MachineProvider Interface
3
+ *
4
+ * Abstraction for accessing machine resources (projects, workspaces, sessions).
5
+ * Implementations include:
6
+ * - LocalMachineProvider: Direct access to local machine
7
+ * - RemoteMachineProvider: Access via encrypted relay connection
8
+ */
9
+
10
+ import type {
11
+ MachineInfo,
12
+ Project,
13
+ Workspace,
14
+ InboxItem,
15
+ SessionStream,
16
+ } from '../types.js';
17
+
18
+ /**
19
+ * Options for creating a new session
20
+ */
21
+ export interface CreateSessionOptions {
22
+ /** Session name (user-friendly identifier) */
23
+ sessionName?: string;
24
+ /** Working directory for the session */
25
+ cwd?: string;
26
+ /** Shell to use (defaults to $SHELL or /bin/bash) */
27
+ shell?: string;
28
+ /** Environment variables */
29
+ env?: Record<string, string>;
30
+ /** Initial terminal size */
31
+ cols?: number;
32
+ rows?: number;
33
+ }
34
+
35
+ /**
36
+ * Options for attaching to an existing session
37
+ */
38
+ export interface AttachSessionOptions {
39
+ /** Terminal size */
40
+ cols: number;
41
+ rows: number;
42
+ /** Client type for session tracking */
43
+ clientType?: 'cli' | 'web';
44
+ /** Force attach (detach other clients) */
45
+ force?: boolean;
46
+ }
47
+
48
+ /**
49
+ * MachineProvider interface
50
+ *
51
+ * Provides a unified API for accessing machine resources,
52
+ * whether local or remote.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * // Local machine
57
+ * const local = new LocalMachineProvider();
58
+ * const projects = await local.listProjects();
59
+ *
60
+ * // Remote machine
61
+ * const remote = new RemoteMachineProvider(relayClient, machineId);
62
+ * const workspaces = await remote.listWorkspaces('my-project');
63
+ * ```
64
+ */
65
+ export interface MachineProvider {
66
+ /**
67
+ * Get information about this machine
68
+ */
69
+ getMachineInfo(): Promise<MachineInfo>;
70
+
71
+ /**
72
+ * List all projects on this machine
73
+ */
74
+ listProjects(): Promise<Project[]>;
75
+
76
+ /**
77
+ * List workspaces for a project
78
+ *
79
+ * @param projectName - Name of the project
80
+ */
81
+ listWorkspaces(projectName: string): Promise<Workspace[]>;
82
+
83
+ /**
84
+ * Create a new session in a workspace
85
+ *
86
+ * @param projectName - Name of the project
87
+ * @param workspaceName - Name of the workspace
88
+ * @param options - Session creation options
89
+ * @returns Session ID of the created session
90
+ */
91
+ createSession(
92
+ projectName: string,
93
+ workspaceName: string,
94
+ options?: CreateSessionOptions
95
+ ): Promise<string>;
96
+
97
+ /**
98
+ * Attach to an existing session
99
+ *
100
+ * @param sessionId - ID of the session to attach to
101
+ * @param options - Attachment options
102
+ * @returns Stream for terminal I/O
103
+ */
104
+ attachSession(
105
+ sessionId: string,
106
+ options: AttachSessionOptions
107
+ ): Promise<SessionStream>;
108
+
109
+ /**
110
+ * Detach from a session
111
+ *
112
+ * @param sessionId - ID of the session to detach from
113
+ */
114
+ detachSession(sessionId: string): Promise<void>;
115
+
116
+ /**
117
+ * Get inbox notifications
118
+ */
119
+ getInbox(): Promise<InboxItem[]>;
120
+
121
+ /**
122
+ * Mark inbox item as read
123
+ *
124
+ * @param itemId - ID of the inbox item
125
+ */
126
+ markInboxRead(itemId: string): Promise<void>;
127
+
128
+ /**
129
+ * Clear all inbox items
130
+ */
131
+ clearInbox(): Promise<void>;
132
+
133
+ /**
134
+ * Dispose of the provider and clean up resources
135
+ */
136
+ dispose(): void;
137
+ }
138
+
139
+ /**
140
+ * Events emitted by MachineProvider
141
+ */
142
+ export type MachineProviderEvent =
143
+ | { type: 'connected' }
144
+ | { type: 'disconnected'; reason: string }
145
+ | { type: 'error'; error: Error }
146
+ | { type: 'inbox_updated'; items: InboxItem[] }
147
+ | { type: 'session_updated'; sessionId: string };
148
+
149
+ /**
150
+ * Event handler for MachineProvider events
151
+ */
152
+ export type MachineProviderEventHandler = (event: MachineProviderEvent) => void;
153
+
154
+ /**
155
+ * Extended MachineProvider with event support
156
+ */
157
+ export interface EventedMachineProvider extends MachineProvider {
158
+ /**
159
+ * Subscribe to provider events
160
+ *
161
+ * @param handler - Event handler
162
+ * @returns Unsubscribe function
163
+ */
164
+ onEvent(handler: MachineProviderEventHandler): () => void;
165
+ }