gitspace 0.2.0-rc.2 → 0.2.0-rc.21

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 (316) hide show
  1. package/README.md +68 -38
  2. package/package.json +36 -25
  3. package/.claude/settings.local.json +0 -21
  4. package/.gitspace/bundle.json +0 -50
  5. package/.gitspace/select/01-status.sh +0 -40
  6. package/.gitspace/setup/01-install-deps.sh +0 -12
  7. package/.gitspace/setup/02-typecheck.sh +0 -16
  8. package/AGENTS.md +0 -439
  9. package/CLAUDE.md +0 -1
  10. package/bun.lock +0 -647
  11. package/docs/CONNECTION.md +0 -623
  12. package/docs/GATEWAY-WORKER.md +0 -319
  13. package/docs/GETTING-STARTED.md +0 -448
  14. package/docs/GITSPACE-PLATFORM.md +0 -1819
  15. package/docs/INFRASTRUCTURE.md +0 -1347
  16. package/docs/PROTOCOL.md +0 -619
  17. package/docs/QUICKSTART.md +0 -174
  18. package/docs/RELAY.md +0 -327
  19. package/docs/REMOTE-DESIGN.md +0 -549
  20. package/docs/ROADMAP.md +0 -564
  21. package/docs/SITE_DOCS_FIGMA_MAKE.md +0 -1167
  22. package/docs/STACK-DESIGN.md +0 -588
  23. package/docs/UNIFIED_ARCHITECTURE.md +0 -292
  24. package/experiments/pty-benchmark.ts +0 -148
  25. package/experiments/pty-latency.ts +0 -100
  26. package/experiments/router/client.ts +0 -199
  27. package/experiments/router/protocol.ts +0 -74
  28. package/experiments/router/router.ts +0 -217
  29. package/experiments/router/session.ts +0 -180
  30. package/experiments/router/test.ts +0 -133
  31. package/experiments/socket-bandwidth.ts +0 -77
  32. package/homebrew/gitspace.rb +0 -45
  33. package/landing-page/ATTRIBUTIONS.md +0 -3
  34. package/landing-page/README.md +0 -11
  35. package/landing-page/bun.lock +0 -801
  36. package/landing-page/guidelines/Guidelines.md +0 -61
  37. package/landing-page/index.html +0 -37
  38. package/landing-page/package.json +0 -90
  39. package/landing-page/postcss.config.mjs +0 -15
  40. package/landing-page/public/_redirects +0 -1
  41. package/landing-page/public/favicon.png +0 -0
  42. package/landing-page/src/app/App.tsx +0 -53
  43. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +0 -27
  44. package/landing-page/src/app/components/ui/accordion.tsx +0 -66
  45. package/landing-page/src/app/components/ui/alert-dialog.tsx +0 -157
  46. package/landing-page/src/app/components/ui/alert.tsx +0 -66
  47. package/landing-page/src/app/components/ui/aspect-ratio.tsx +0 -11
  48. package/landing-page/src/app/components/ui/avatar.tsx +0 -53
  49. package/landing-page/src/app/components/ui/badge.tsx +0 -46
  50. package/landing-page/src/app/components/ui/breadcrumb.tsx +0 -109
  51. package/landing-page/src/app/components/ui/button.tsx +0 -57
  52. package/landing-page/src/app/components/ui/calendar.tsx +0 -75
  53. package/landing-page/src/app/components/ui/card.tsx +0 -92
  54. package/landing-page/src/app/components/ui/carousel.tsx +0 -241
  55. package/landing-page/src/app/components/ui/chart.tsx +0 -353
  56. package/landing-page/src/app/components/ui/checkbox.tsx +0 -32
  57. package/landing-page/src/app/components/ui/collapsible.tsx +0 -33
  58. package/landing-page/src/app/components/ui/command.tsx +0 -177
  59. package/landing-page/src/app/components/ui/context-menu.tsx +0 -252
  60. package/landing-page/src/app/components/ui/dialog.tsx +0 -135
  61. package/landing-page/src/app/components/ui/drawer.tsx +0 -132
  62. package/landing-page/src/app/components/ui/dropdown-menu.tsx +0 -257
  63. package/landing-page/src/app/components/ui/form.tsx +0 -168
  64. package/landing-page/src/app/components/ui/hover-card.tsx +0 -44
  65. package/landing-page/src/app/components/ui/input-otp.tsx +0 -77
  66. package/landing-page/src/app/components/ui/input.tsx +0 -21
  67. package/landing-page/src/app/components/ui/label.tsx +0 -24
  68. package/landing-page/src/app/components/ui/menubar.tsx +0 -276
  69. package/landing-page/src/app/components/ui/navigation-menu.tsx +0 -168
  70. package/landing-page/src/app/components/ui/pagination.tsx +0 -127
  71. package/landing-page/src/app/components/ui/popover.tsx +0 -48
  72. package/landing-page/src/app/components/ui/progress.tsx +0 -31
  73. package/landing-page/src/app/components/ui/radio-group.tsx +0 -45
  74. package/landing-page/src/app/components/ui/resizable.tsx +0 -56
  75. package/landing-page/src/app/components/ui/scroll-area.tsx +0 -58
  76. package/landing-page/src/app/components/ui/select.tsx +0 -189
  77. package/landing-page/src/app/components/ui/separator.tsx +0 -28
  78. package/landing-page/src/app/components/ui/sheet.tsx +0 -139
  79. package/landing-page/src/app/components/ui/sidebar.tsx +0 -726
  80. package/landing-page/src/app/components/ui/skeleton.tsx +0 -13
  81. package/landing-page/src/app/components/ui/slider.tsx +0 -63
  82. package/landing-page/src/app/components/ui/sonner.tsx +0 -25
  83. package/landing-page/src/app/components/ui/switch.tsx +0 -31
  84. package/landing-page/src/app/components/ui/table.tsx +0 -116
  85. package/landing-page/src/app/components/ui/tabs.tsx +0 -66
  86. package/landing-page/src/app/components/ui/textarea.tsx +0 -18
  87. package/landing-page/src/app/components/ui/toggle-group.tsx +0 -73
  88. package/landing-page/src/app/components/ui/toggle.tsx +0 -47
  89. package/landing-page/src/app/components/ui/tooltip.tsx +0 -61
  90. package/landing-page/src/app/components/ui/use-mobile.ts +0 -21
  91. package/landing-page/src/app/components/ui/utils.ts +0 -6
  92. package/landing-page/src/components/docs/DocsContent.tsx +0 -718
  93. package/landing-page/src/components/docs/DocsSidebar.tsx +0 -84
  94. package/landing-page/src/components/landing/CTA.tsx +0 -59
  95. package/landing-page/src/components/landing/Comparison.tsx +0 -84
  96. package/landing-page/src/components/landing/FaultyTerminal.tsx +0 -424
  97. package/landing-page/src/components/landing/Features.tsx +0 -201
  98. package/landing-page/src/components/landing/Hero.tsx +0 -142
  99. package/landing-page/src/components/landing/Pricing.tsx +0 -140
  100. package/landing-page/src/components/landing/Roadmap.tsx +0 -86
  101. package/landing-page/src/components/landing/Security.tsx +0 -81
  102. package/landing-page/src/components/landing/TerminalWindow.tsx +0 -27
  103. package/landing-page/src/components/landing/UseCases.tsx +0 -55
  104. package/landing-page/src/components/landing/Workflow.tsx +0 -101
  105. package/landing-page/src/components/layout/DashboardNavbar.tsx +0 -37
  106. package/landing-page/src/components/layout/Footer.tsx +0 -55
  107. package/landing-page/src/components/layout/LandingNavbar.tsx +0 -82
  108. package/landing-page/src/components/ui/badge.tsx +0 -39
  109. package/landing-page/src/components/ui/breadcrumb.tsx +0 -115
  110. package/landing-page/src/components/ui/button.tsx +0 -57
  111. package/landing-page/src/components/ui/card.tsx +0 -79
  112. package/landing-page/src/components/ui/mock-terminal.tsx +0 -68
  113. package/landing-page/src/components/ui/separator.tsx +0 -28
  114. package/landing-page/src/lib/utils.ts +0 -6
  115. package/landing-page/src/main.tsx +0 -10
  116. package/landing-page/src/pages/Dashboard.tsx +0 -133
  117. package/landing-page/src/pages/DocsPage.tsx +0 -79
  118. package/landing-page/src/pages/LandingPage.tsx +0 -31
  119. package/landing-page/src/pages/TerminalView.tsx +0 -106
  120. package/landing-page/src/styles/fonts.css +0 -0
  121. package/landing-page/src/styles/index.css +0 -3
  122. package/landing-page/src/styles/tailwind.css +0 -4
  123. package/landing-page/src/styles/theme.css +0 -181
  124. package/landing-page/vite.config.ts +0 -19
  125. package/npm/darwin-arm64/bin/gssh +0 -0
  126. package/npm/darwin-arm64/package.json +0 -20
  127. package/scripts/build.ts +0 -298
  128. package/scripts/release.ts +0 -140
  129. package/src/__tests__/test-utils.ts +0 -298
  130. package/src/commands/__tests__/serve-messages.test.ts +0 -190
  131. package/src/commands/access.ts +0 -298
  132. package/src/commands/add.ts +0 -452
  133. package/src/commands/auth.ts +0 -364
  134. package/src/commands/connect.ts +0 -287
  135. package/src/commands/directory.ts +0 -16
  136. package/src/commands/host.ts +0 -396
  137. package/src/commands/identity.ts +0 -184
  138. package/src/commands/list.ts +0 -200
  139. package/src/commands/relay.ts +0 -315
  140. package/src/commands/remove.ts +0 -241
  141. package/src/commands/serve.ts +0 -1493
  142. package/src/commands/share.ts +0 -456
  143. package/src/commands/status.ts +0 -125
  144. package/src/commands/switch.ts +0 -353
  145. package/src/commands/tmux.ts +0 -317
  146. package/src/core/__tests__/access.test.ts +0 -240
  147. package/src/core/access.ts +0 -277
  148. package/src/core/bundle.ts +0 -342
  149. package/src/core/config.ts +0 -510
  150. package/src/core/git.ts +0 -317
  151. package/src/core/github.ts +0 -151
  152. package/src/core/identity.ts +0 -631
  153. package/src/core/linear.ts +0 -225
  154. package/src/core/shell.ts +0 -161
  155. package/src/core/trusted-relays.ts +0 -315
  156. package/src/index.ts +0 -810
  157. package/src/lib/remote-session/index.ts +0 -7
  158. package/src/lib/remote-session/protocol.ts +0 -267
  159. package/src/lib/remote-session/session-handler.ts +0 -581
  160. package/src/lib/remote-session/workspace-scanner.ts +0 -167
  161. package/src/lib/tmux-lite/README.md +0 -81
  162. package/src/lib/tmux-lite/cli.ts +0 -796
  163. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +0 -349
  164. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +0 -291
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +0 -142
  166. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +0 -339
  167. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +0 -477
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +0 -499
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +0 -371
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +0 -573
  171. package/src/lib/tmux-lite/crypto/access-control.test.ts +0 -512
  172. package/src/lib/tmux-lite/crypto/access-control.ts +0 -320
  173. package/src/lib/tmux-lite/crypto/frames.test.ts +0 -262
  174. package/src/lib/tmux-lite/crypto/frames.ts +0 -141
  175. package/src/lib/tmux-lite/crypto/handshake.ts +0 -894
  176. package/src/lib/tmux-lite/crypto/identity.test.ts +0 -220
  177. package/src/lib/tmux-lite/crypto/identity.ts +0 -286
  178. package/src/lib/tmux-lite/crypto/index.ts +0 -51
  179. package/src/lib/tmux-lite/crypto/invites.test.ts +0 -381
  180. package/src/lib/tmux-lite/crypto/invites.ts +0 -215
  181. package/src/lib/tmux-lite/crypto/keyexchange.ts +0 -435
  182. package/src/lib/tmux-lite/crypto/keys.test.ts +0 -58
  183. package/src/lib/tmux-lite/crypto/keys.ts +0 -47
  184. package/src/lib/tmux-lite/crypto/secretbox.test.ts +0 -169
  185. package/src/lib/tmux-lite/crypto/secretbox.ts +0 -124
  186. package/src/lib/tmux-lite/handshake-handler.ts +0 -451
  187. package/src/lib/tmux-lite/protocol.test.ts +0 -307
  188. package/src/lib/tmux-lite/protocol.ts +0 -266
  189. package/src/lib/tmux-lite/relay-client.ts +0 -506
  190. package/src/lib/tmux-lite/server.ts +0 -1250
  191. package/src/lib/tmux-lite/shell-integration.sh +0 -37
  192. package/src/lib/tmux-lite/terminal-queries.test.ts +0 -54
  193. package/src/lib/tmux-lite/terminal-queries.ts +0 -49
  194. package/src/relay/__tests__/e2e-flow.test.ts +0 -1284
  195. package/src/relay/__tests__/helpers/auth.ts +0 -354
  196. package/src/relay/__tests__/helpers/ports.ts +0 -51
  197. package/src/relay/__tests__/protocol-validation.test.ts +0 -265
  198. package/src/relay/authorization.ts +0 -303
  199. package/src/relay/embedded-assets.generated.d.ts +0 -15
  200. package/src/relay/identity.ts +0 -352
  201. package/src/relay/index.ts +0 -57
  202. package/src/relay/pipes.test.ts +0 -427
  203. package/src/relay/pipes.ts +0 -195
  204. package/src/relay/protocol.ts +0 -804
  205. package/src/relay/registries.test.ts +0 -437
  206. package/src/relay/registries.ts +0 -593
  207. package/src/relay/server.test.ts +0 -1323
  208. package/src/relay/server.ts +0 -1092
  209. package/src/relay/signing.ts +0 -238
  210. package/src/relay/types.ts +0 -69
  211. package/src/serve/client-session-manager.ts +0 -622
  212. package/src/serve/daemon.ts +0 -497
  213. package/src/serve/pty-session.ts +0 -236
  214. package/src/serve/types.ts +0 -169
  215. package/src/shared/components/Flow.tsx +0 -453
  216. package/src/shared/components/Flow.tui.tsx +0 -343
  217. package/src/shared/components/Flow.web.tsx +0 -442
  218. package/src/shared/components/Inbox.tsx +0 -446
  219. package/src/shared/components/Inbox.tui.tsx +0 -262
  220. package/src/shared/components/Inbox.web.tsx +0 -329
  221. package/src/shared/components/MachineList.tsx +0 -187
  222. package/src/shared/components/MachineList.tui.tsx +0 -161
  223. package/src/shared/components/MachineList.web.tsx +0 -210
  224. package/src/shared/components/ProjectList.tsx +0 -176
  225. package/src/shared/components/ProjectList.tui.tsx +0 -109
  226. package/src/shared/components/ProjectList.web.tsx +0 -143
  227. package/src/shared/components/SpacesBrowser.tsx +0 -332
  228. package/src/shared/components/SpacesBrowser.tui.tsx +0 -163
  229. package/src/shared/components/SpacesBrowser.web.tsx +0 -221
  230. package/src/shared/components/index.ts +0 -103
  231. package/src/shared/hooks/index.ts +0 -16
  232. package/src/shared/hooks/useNavigation.ts +0 -226
  233. package/src/shared/index.ts +0 -122
  234. package/src/shared/providers/LocalMachineProvider.ts +0 -425
  235. package/src/shared/providers/MachineProvider.ts +0 -165
  236. package/src/shared/providers/RemoteMachineProvider.ts +0 -444
  237. package/src/shared/providers/index.ts +0 -26
  238. package/src/shared/types.ts +0 -145
  239. package/src/tui/adapters.ts +0 -120
  240. package/src/tui/app.tsx +0 -1816
  241. package/src/tui/components/Terminal.tsx +0 -580
  242. package/src/tui/hooks/index.ts +0 -35
  243. package/src/tui/hooks/useAppState.ts +0 -314
  244. package/src/tui/hooks/useDaemonStatus.ts +0 -174
  245. package/src/tui/hooks/useInboxTUI.ts +0 -113
  246. package/src/tui/hooks/useRemoteMachines.ts +0 -209
  247. package/src/tui/index.ts +0 -24
  248. package/src/tui/state.ts +0 -299
  249. package/src/tui/terminal-bracketed-paste.test.ts +0 -45
  250. package/src/tui/terminal-bracketed-paste.ts +0 -47
  251. package/src/types/bundle.ts +0 -112
  252. package/src/types/config.ts +0 -89
  253. package/src/types/errors.ts +0 -206
  254. package/src/types/identity.ts +0 -284
  255. package/src/types/workspace-fuzzy.ts +0 -49
  256. package/src/types/workspace.ts +0 -151
  257. package/src/utils/bun-socket-writer.ts +0 -80
  258. package/src/utils/deps.ts +0 -127
  259. package/src/utils/fuzzy-match.ts +0 -125
  260. package/src/utils/logger.ts +0 -127
  261. package/src/utils/markdown.ts +0 -254
  262. package/src/utils/onboarding.ts +0 -229
  263. package/src/utils/prompts.ts +0 -114
  264. package/src/utils/run-commands.ts +0 -112
  265. package/src/utils/run-scripts.ts +0 -142
  266. package/src/utils/sanitize.ts +0 -98
  267. package/src/utils/secrets.ts +0 -122
  268. package/src/utils/shell-escape.ts +0 -40
  269. package/src/utils/utf8.ts +0 -79
  270. package/src/utils/workspace-state.ts +0 -47
  271. package/src/web/README.md +0 -73
  272. package/src/web/bun.lock +0 -575
  273. package/src/web/eslint.config.js +0 -23
  274. package/src/web/index.html +0 -16
  275. package/src/web/package.json +0 -37
  276. package/src/web/public/vite.svg +0 -1
  277. package/src/web/src/App.tsx +0 -604
  278. package/src/web/src/assets/react.svg +0 -1
  279. package/src/web/src/components/Terminal.tsx +0 -207
  280. package/src/web/src/hooks/useRelayConnection.ts +0 -224
  281. package/src/web/src/hooks/useTerminal.ts +0 -699
  282. package/src/web/src/index.css +0 -55
  283. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +0 -1158
  284. package/src/web/src/lib/crypto/frames.ts +0 -205
  285. package/src/web/src/lib/crypto/handshake.ts +0 -396
  286. package/src/web/src/lib/crypto/identity.ts +0 -128
  287. package/src/web/src/lib/crypto/keyexchange.ts +0 -246
  288. package/src/web/src/lib/crypto/relay-signing.ts +0 -53
  289. package/src/web/src/lib/invite.ts +0 -58
  290. package/src/web/src/lib/storage/identity-store.ts +0 -94
  291. package/src/web/src/main.tsx +0 -10
  292. package/src/web/src/types/identity.ts +0 -45
  293. package/src/web/tsconfig.app.json +0 -28
  294. package/src/web/tsconfig.json +0 -7
  295. package/src/web/tsconfig.node.json +0 -26
  296. package/src/web/vite.config.ts +0 -31
  297. package/todo-security.md +0 -92
  298. package/tsconfig.json +0 -23
  299. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  300. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  305. package/worker/bun.lock +0 -237
  306. package/worker/package.json +0 -22
  307. package/worker/schema.sql +0 -96
  308. package/worker/src/handlers/auth.ts +0 -451
  309. package/worker/src/handlers/subdomains.ts +0 -376
  310. package/worker/src/handlers/user.ts +0 -98
  311. package/worker/src/index.ts +0 -70
  312. package/worker/src/middleware/auth.ts +0 -152
  313. package/worker/src/services/cloudflare.ts +0 -609
  314. package/worker/src/types.ts +0 -96
  315. package/worker/tsconfig.json +0 -15
  316. package/worker/wrangler.toml +0 -26
@@ -1,796 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * tmux-lite CLI and API
4
- *
5
- * CLI Commands:
6
- * tl new [name] Create new session
7
- * tl a|attach [id] Attach to session
8
- * tl ls|list List sessions
9
- * tl kill <id> Kill a session
10
- * tl kill-server Stop the server
11
- *
12
- * API: Import and use the exported functions
13
- */
14
-
15
- import { spawn } from "bun";
16
- import { existsSync, readFileSync, unlinkSync } from "fs";
17
- import { select } from "@inquirer/prompts";
18
- import { createBufferedSocketWriter } from "../../utils/bun-socket-writer";
19
- import {
20
- getRouterSocket,
21
- getPidFile,
22
- PROTOCOL_VERSION,
23
- PACKAGE_VERSION,
24
- type Command,
25
- type Response,
26
- type Session,
27
- type SessionEvent,
28
- type InboxItem,
29
- encodeRouterMessage,
30
- decodeRouterMessages,
31
- encodeControl,
32
- encodePTY,
33
- parseFrames,
34
- decodeControl,
35
- FrameType,
36
- } from "./protocol";
37
-
38
- // Re-export types
39
- export type { Session, InboxItem, Command, Response };
40
-
41
- // Re-export constants
42
- export { PROTOCOL_VERSION, PACKAGE_VERSION, getRouterSocket, getPidFile };
43
-
44
- /** Status response from server */
45
- export interface ServerStatus {
46
- version: string;
47
- protocol: number;
48
- pid: number;
49
- uptime: number;
50
- sessions: number;
51
- attached: number;
52
- }
53
-
54
- // Terminal reset - RIS (Reset to Initial State) resets everything
55
- const TERM_RESET = "\x1bc";
56
-
57
- const SERVER_SCRIPT = `${import.meta.dir}/server.ts`;
58
-
59
- // CLI args
60
- const rawArgs = process.argv.slice(2);
61
- const isTestMode = rawArgs.includes("--test");
62
- const args = rawArgs.filter(arg => arg !== "--test");
63
- const cmd = args[0] || "list";
64
-
65
- if (isTestMode) {
66
- process.env.TMUX_LITE_SOCKET = "/tmp/tmux-lite-test.sock";
67
- process.env.TMUX_LITE_SESSION_DIR = "/tmp/tmux-lite-test";
68
- }
69
-
70
- const getServerCommand = (): string[] => (
71
- isTestMode ? ["bun", "run", SERVER_SCRIPT, "--test"] : ["bun", "run", SERVER_SCRIPT]
72
- );
73
-
74
- // Check if we're already inside a tmux-lite session
75
- export function isNested(): boolean {
76
- return !!process.env.TMUX_LITE;
77
- }
78
-
79
- function checkNested(): boolean {
80
- if (isNested()) {
81
- console.error("Error: Already inside tmux-lite session " + process.env.TMUX_LITE);
82
- console.error("Nested sessions are not supported. Detach first with Ctrl+Esc.");
83
- return true;
84
- }
85
- return false;
86
- }
87
-
88
- // Check if server is running
89
- export async function isServerRunning(): Promise<boolean> {
90
- const routerSocket = getRouterSocket();
91
- if (!existsSync(routerSocket)) return false;
92
- try {
93
- await send({ type: "list" });
94
- return true;
95
- } catch {
96
- return false;
97
- }
98
- }
99
-
100
- // Start server if not running
101
- export async function ensureServer(): Promise<void> {
102
- if (await isServerRunning()) return;
103
-
104
- spawn({
105
- cmd: getServerCommand(),
106
- stdout: "ignore",
107
- stderr: "ignore",
108
- });
109
-
110
- for (let i = 0; i < 30; i++) {
111
- await Bun.sleep(100);
112
- if (await isServerRunning()) return;
113
- }
114
- throw new Error("Failed to start tmux-lite server");
115
- }
116
-
117
- /**
118
- * Check if a process with given PID is running
119
- */
120
- export function isProcessRunning(pid: number): boolean {
121
- try {
122
- // Signal 0 doesn't kill - just checks if process exists
123
- process.kill(pid, 0);
124
- return true;
125
- } catch {
126
- return false;
127
- }
128
- }
129
-
130
- /**
131
- * Get server PID from PID file
132
- * Returns null if PID file doesn't exist or is invalid
133
- */
134
- export function getServerPid(): number | null {
135
- const pidFile = getPidFile();
136
- if (!existsSync(pidFile)) return null;
137
-
138
- try {
139
- const content = readFileSync(pidFile, "utf-8").trim();
140
- const pid = parseInt(content, 10);
141
- if (isNaN(pid) || pid <= 0) return null;
142
- return pid;
143
- } catch {
144
- return null;
145
- }
146
- }
147
-
148
- /**
149
- * Clean up stale PID file if process is not running
150
- * Returns true if cleanup was needed
151
- */
152
- export function cleanupStalePidFile(): boolean {
153
- const pid = getServerPid();
154
- if (pid === null) return false;
155
-
156
- if (!isProcessRunning(pid)) {
157
- // Process is dead, clean up stale files
158
- const pidFile = getPidFile();
159
- const routerSocket = getRouterSocket();
160
- try { unlinkSync(pidFile); } catch {}
161
- try { unlinkSync(routerSocket); } catch {}
162
- return true;
163
- }
164
- return false;
165
- }
166
-
167
- /**
168
- * Get server version info
169
- */
170
- export async function getVersion(): Promise<{ version: string; protocol: number }> {
171
- await ensureServer();
172
- const res = await send({ type: "version" });
173
- if (res.type === "version") {
174
- return { version: res.version, protocol: res.protocol };
175
- }
176
- throw new Error("Unexpected response");
177
- }
178
-
179
- /**
180
- * Get server status (version + stats)
181
- */
182
- export async function getStatus(): Promise<ServerStatus> {
183
- await ensureServer();
184
- const res = await send({ type: "status" });
185
- if (res.type === "status") {
186
- return {
187
- version: res.version,
188
- protocol: res.protocol,
189
- pid: res.pid,
190
- uptime: res.uptime,
191
- sessions: res.sessions,
192
- attached: res.attached,
193
- };
194
- }
195
- throw new Error("Unexpected response");
196
- }
197
-
198
- /**
199
- * Alias for killServer - stops the server daemon
200
- */
201
- export const stopServer = killServer;
202
-
203
- // Send command to server
204
- export async function send(cmd: Command): Promise<Response> {
205
- return new Promise(async (resolve, reject) => {
206
- let buffer: Buffer = Buffer.alloc(0);
207
- let settled = false;
208
- let socketRef: Awaited<ReturnType<typeof Bun.connect>> | null = null;
209
- let socketWriter: ReturnType<typeof createBufferedSocketWriter> | null = null;
210
-
211
- const fail = (err: Error) => {
212
- if (settled) return;
213
- settled = true;
214
- socketRef?.end();
215
- reject(err);
216
- };
217
-
218
- try {
219
- const routerSocket = getRouterSocket();
220
- const socket = await Bun.connect({
221
- unix: routerSocket,
222
- socket: {
223
- drain() {
224
- socketWriter?.flush();
225
- },
226
- data(socket, data) {
227
- if (settled) return;
228
- buffer = Buffer.concat([buffer, Buffer.from(data)]);
229
- let decoded;
230
- try {
231
- decoded = decodeRouterMessages(buffer);
232
- } catch (err) {
233
- fail(err instanceof Error ? err : new Error("Invalid response"));
234
- return;
235
- }
236
- buffer = decoded.remaining as Buffer;
237
- if (decoded.messages.length > 0) {
238
- settled = true;
239
- resolve(decoded.messages[0] as Response);
240
- socket.end();
241
- }
242
- },
243
- close() {
244
- if (!settled) {
245
- fail(new Error("Connection closed before response"));
246
- }
247
- },
248
- error(_, e) { fail(e); },
249
- connectError(_, e) { fail(e); }
250
- }
251
- });
252
- socketRef = socket;
253
- socketWriter = createBufferedSocketWriter(socket);
254
- socketWriter.write(encodeRouterMessage(cmd));
255
- } catch (e) {
256
- fail(e instanceof Error ? e : new Error(String(e)));
257
- }
258
- });
259
- }
260
-
261
- // === API convenience functions ===
262
-
263
- export async function listSessions(): Promise<Session[]> {
264
- await ensureServer();
265
- const res = await send({ type: "list" });
266
- if (res.type === "sessions") return res.sessions;
267
- throw new Error("Unexpected response");
268
- }
269
-
270
- export async function createSession(name: string, cwd: string): Promise<Session> {
271
- await ensureServer();
272
- const res = await send({ type: "new", name, cwd });
273
- if (res.type === "session") return res.session;
274
- if (res.type === "error") throw new Error(res.message);
275
- throw new Error("Unexpected response");
276
- }
277
-
278
- export async function killSession(id: string): Promise<void> {
279
- await ensureServer();
280
- const res = await send({ type: "kill", id });
281
- if (res.type === "error") throw new Error(res.message);
282
- }
283
-
284
- export async function killServer(): Promise<void> {
285
- if (!(await isServerRunning())) return;
286
- await send({ type: "kill-server" });
287
- }
288
-
289
- export async function getInbox(): Promise<InboxItem[]> {
290
- await ensureServer();
291
- const res = await send({ type: "inbox" });
292
- if (res.type === "inbox") return res.items;
293
- throw new Error("Unexpected response");
294
- }
295
-
296
- export async function getUnreadCount(): Promise<number> {
297
- const items = await getInbox();
298
- return items.filter(i => !i.read).length;
299
- }
300
-
301
- export async function clearInbox(id?: string): Promise<void> {
302
- await ensureServer();
303
- await send({ type: "inbox-clear", id });
304
- }
305
-
306
- export async function markInboxRead(id: string): Promise<void> {
307
- await ensureServer();
308
- await send({ type: "inbox-read", id });
309
- }
310
-
311
- // Format session for display
312
- function formatSession(s: Session): string {
313
- const age = Math.floor((Date.now() - s.createdAt) / 1000);
314
- const ageStr = age < 60 ? `${age}s` : age < 3600 ? `${Math.floor(age/60)}m` : `${Math.floor(age/3600)}h`;
315
- const status = s.attached ? "\x1b[32m●\x1b[0m" : "\x1b[90m○\x1b[0m";
316
- const title = s.processTitle ? ` \x1b[33m[${s.processTitle}]\x1b[0m` : "";
317
- return `${status} ${s.id}: ${s.name} (${ageStr})${title} ${s.cwd}`;
318
- }
319
-
320
- // Ctrl+Esc sequences (different terminals send different formats)
321
- const CTRL_ESC_CSI_U = Buffer.from([0x1b, 0x5b, 0x32, 0x37, 0x3b, 0x35, 0x75]); // ESC [ 27;5u
322
- const CTRL_ESC_XTERM = Buffer.from([0x1b, 0x5b, 0x32, 0x37, 0x3b, 0x35, 0x3b, 0x32, 0x37, 0x7e]); // ESC [ 27;5;27 ~
323
- const BRACKETED_PASTE_START = Buffer.from([0x1b, 0x5b, 0x32, 0x30, 0x30, 0x7e]); // ESC [ 200 ~
324
- const BRACKETED_PASTE_END = Buffer.from([0x1b, 0x5b, 0x32, 0x30, 0x31, 0x7e]); // ESC [ 201 ~
325
-
326
- function containsCtrlEsc(buf: Buffer): number {
327
- const idx1 = buf.indexOf(CTRL_ESC_CSI_U);
328
- const idx2 = buf.indexOf(CTRL_ESC_XTERM);
329
- if (idx1 === -1) return idx2;
330
- if (idx2 === -1) return idx1;
331
- return Math.min(idx1, idx2);
332
- }
333
-
334
- export type AttachResult =
335
- | { type: "detached" }
336
- | { type: "exited"; code: number }
337
- | { type: "kicked" }
338
- | { type: "error"; message: string };
339
-
340
- /**
341
- * Attach to a session interactively.
342
- * Takes over stdin/stdout. Returns when session ends or user detaches.
343
- * @param session Session to attach to
344
- * @param quiet If true, don't print attach/detach messages
345
- */
346
- export async function attach(session: Session, quiet: boolean = false): Promise<AttachResult> {
347
- if (!quiet) {
348
- console.log(`Attaching to ${session.name}...`);
349
- console.log("Ctrl+Esc to detach\n");
350
- }
351
-
352
- return new Promise(async (resolve) => {
353
- let buffer = Buffer.alloc(0);
354
- let pendingSeq = Buffer.alloc(0);
355
- let inBracketedPaste = false;
356
- let resolved = false;
357
- let stdinListener: ((chunk: Buffer) => void) | null = null;
358
- let socket: Awaited<ReturnType<typeof Bun.connect>> | null = null;
359
- let socketWriter: ReturnType<typeof createBufferedSocketWriter> | null = null;
360
- let onResize: (() => void) | null = null;
361
- let lastSize = { cols: 0, rows: 0 };
362
-
363
- const cleanup = (result: AttachResult) => {
364
- if (resolved) return;
365
- resolved = true;
366
- if (stdinListener) {
367
- process.stdin.removeListener("data", stdinListener);
368
- }
369
- process.stdin.setRawMode(false);
370
- process.stdin.pause();
371
- process.stdout.write(TERM_RESET);
372
- if (onResize) {
373
- process.removeListener("SIGWINCH", onResize);
374
- }
375
- socket = null;
376
- socketWriter = null;
377
- if (!quiet) {
378
- if (result.type === "detached") console.log("\n[detached]");
379
- else if (result.type === "exited") console.log(`\n[exited: ${result.code}]`);
380
- else if (result.type === "kicked") console.log("\n[kicked - another client took over]");
381
- else if (result.type === "error") console.error("\n[error]", result.message);
382
- }
383
- resolve(result);
384
- };
385
-
386
- const getTermSize = () => {
387
- let cols = process.stdout.columns || 0;
388
- let rows = process.stdout.rows || 0;
389
- if (cols <= 0 || rows <= 0) {
390
- const size = (process.stdout as { getWindowSize?: () => number[] }).getWindowSize?.();
391
- if (Array.isArray(size) && size.length >= 2) {
392
- cols = size[0];
393
- rows = size[1];
394
- }
395
- }
396
- return {
397
- cols: cols > 0 ? cols : 80,
398
- rows: rows > 0 ? rows : 24,
399
- };
400
- };
401
-
402
- const sendResize = (force = false) => {
403
- if (!socket) return;
404
- const { cols, rows } = getTermSize();
405
- if (!force && cols === lastSize.cols && rows === lastSize.rows) {
406
- return;
407
- }
408
- lastSize = { cols, rows };
409
- const frame = encodeControl({ type: "resize", cols, rows });
410
- if (socketWriter) socketWriter.write(frame);
411
- else socket.write(frame);
412
- };
413
-
414
- const sendAttachInit = () => {
415
- if (!socket) return;
416
- const { cols, rows } = getTermSize();
417
- const frame = encodeControl({ type: "attach-init", cols, rows, clientType: "cli" });
418
- if (socketWriter) socketWriter.write(frame);
419
- else socket.write(frame);
420
- };
421
-
422
- socket = await Bun.connect({
423
- unix: session.socketPath,
424
- socket: {
425
- drain() {
426
- socketWriter?.flush();
427
- },
428
- data(socket, data) {
429
- let buf = Buffer.from(data);
430
-
431
- if (buffer.length > 0) {
432
- buf = Buffer.concat([buffer, buf]);
433
- }
434
-
435
- // Parse frames from the buffer
436
- let frames;
437
- let remaining;
438
- try {
439
- const result = parseFrames(buf);
440
- frames = result.frames;
441
- remaining = result.remaining;
442
- } catch (err) {
443
- // Protocol error - likely desync or corrupted data
444
- const msg = err instanceof Error ? err.message : 'Frame parse error';
445
- console.error(`[attach] Frame parse error: ${msg}`);
446
- cleanup({ type: "error", message: msg });
447
- return;
448
- }
449
- buffer = Buffer.from(remaining);
450
-
451
- for (const frame of frames) {
452
- if (frame.type === FrameType.CONTROL) {
453
- const event = decodeControl(frame.payload) as SessionEvent;
454
-
455
- if (event.type === "attached") {
456
- // Send a single resize to ensure proper dimensions
457
- sendResize(true);
458
- } else if (event.type === "exited") {
459
- cleanup({ type: "exited", code: event.code });
460
- return;
461
- } else if (event.type === "kicked") {
462
- cleanup({ type: "kicked" });
463
- return;
464
- }
465
- } else if (frame.type === FrameType.PTY) {
466
- process.stdout.write(frame.payload);
467
- }
468
- }
469
- },
470
-
471
- close() {
472
- cleanup({ type: "detached" });
473
- },
474
-
475
- error(_, e) {
476
- cleanup({ type: "error", message: e.message });
477
- }
478
- }
479
- });
480
- socketWriter = createBufferedSocketWriter(socket);
481
-
482
- // Initial resize
483
- sendAttachInit();
484
- sendResize(true);
485
-
486
- onResize = () => {
487
- sendResize();
488
- };
489
- process.on("SIGWINCH", onResize);
490
- process.stdin.setRawMode(true);
491
- process.stdin.resume();
492
-
493
- // Forward stdin with Ctrl+Esc detection
494
- stdinListener = (chunk: Buffer) => {
495
- const combined = pendingSeq.length > 0 ? Buffer.concat([pendingSeq, chunk]) : chunk;
496
- pendingSeq = Buffer.alloc(0);
497
- const out: Buffer[] = [];
498
- let offset = 0;
499
-
500
- const flushOut = () => {
501
- if (out.length > 0 && socket) {
502
- const frame = encodePTY(Buffer.concat(out));
503
- if (socketWriter) socketWriter.write(frame);
504
- else socket.write(frame);
505
- out.length = 0;
506
- }
507
- };
508
-
509
- const getSequences = () => (
510
- inBracketedPaste
511
- ? [BRACKETED_PASTE_START, BRACKETED_PASTE_END]
512
- : [BRACKETED_PASTE_START, BRACKETED_PASTE_END, CTRL_ESC_CSI_U, CTRL_ESC_XTERM]
513
- );
514
-
515
- while (offset < combined.length) {
516
- if (combined[offset] !== 0x1b) {
517
- const nextEsc = combined.indexOf(0x1b, offset + 1);
518
- if (nextEsc === -1) {
519
- out.push(combined.subarray(offset));
520
- offset = combined.length;
521
- } else {
522
- out.push(combined.subarray(offset, nextEsc));
523
- offset = nextEsc;
524
- }
525
- continue;
526
- }
527
-
528
- const sequences = getSequences();
529
- let matched: Buffer | null = null;
530
- for (const seq of sequences) {
531
- if (combined.length - offset >= seq.length &&
532
- combined.subarray(offset, offset + seq.length).equals(seq)) {
533
- matched = seq;
534
- break;
535
- }
536
- }
537
-
538
- if (matched) {
539
- if (matched === CTRL_ESC_CSI_U || matched === CTRL_ESC_XTERM) {
540
- flushOut();
541
- if (socket) {
542
- const frame = encodeControl({ type: "detach" });
543
- if (socketWriter) socketWriter.write(frame);
544
- else socket.write(frame);
545
- }
546
- cleanup({ type: "detached" });
547
- return;
548
- }
549
-
550
- out.push(combined.subarray(offset, offset + matched.length));
551
- if (matched === BRACKETED_PASTE_START) {
552
- inBracketedPaste = true;
553
- } else if (matched === BRACKETED_PASTE_END) {
554
- inBracketedPaste = false;
555
- }
556
- offset += matched.length;
557
- continue;
558
- }
559
-
560
- let possiblePrefix = false;
561
- for (const seq of sequences) {
562
- const remaining = combined.length - offset;
563
- if (remaining < seq.length &&
564
- seq.subarray(0, remaining).equals(combined.subarray(offset))) {
565
- possiblePrefix = true;
566
- break;
567
- }
568
- }
569
-
570
- if (possiblePrefix) {
571
- pendingSeq = Buffer.from(combined.subarray(offset));
572
- break;
573
- }
574
-
575
- out.push(combined.subarray(offset, offset + 1));
576
- offset += 1;
577
- }
578
-
579
- flushOut();
580
- };
581
-
582
- process.stdin.on("data", stdinListener);
583
- });
584
- }
585
-
586
- // Handle attach result and exit with appropriate code
587
- function handleAttachResult(result: AttachResult): void {
588
- if (result.type === "exited") {
589
- process.exit(result.code);
590
- } else if (result.type === "error") {
591
- process.exit(1);
592
- }
593
- // detached and kicked exit cleanly
594
- process.exit(0);
595
- }
596
-
597
- // Main
598
- async function main() {
599
- // Start server if not running
600
- if (!(await isServerRunning())) {
601
- if (cmd === "kill-server") {
602
- console.log("Server not running");
603
- return;
604
- }
605
- console.log("Starting server...");
606
- spawn({
607
- cmd: getServerCommand(),
608
- stdout: "inherit",
609
- stderr: "inherit",
610
- });
611
- await Bun.sleep(300);
612
- if (!(await isServerRunning())) {
613
- console.error("Failed to start server");
614
- process.exit(1);
615
- }
616
- }
617
-
618
- switch (cmd) {
619
- case "new": {
620
- if (checkNested()) process.exit(1);
621
- const name = args[1];
622
- const res = await send({ type: "new", name, cwd: process.cwd() });
623
- if (res.type === "session") {
624
- const result = await attach(res.session);
625
- handleAttachResult(result);
626
- } else if (res.type === "error") {
627
- console.error("Error:", res.message);
628
- }
629
- break;
630
- }
631
-
632
- case "a":
633
- case "attach": {
634
- if (checkNested()) process.exit(1);
635
- const id = args[1];
636
- if (id) {
637
- const res = await send({ type: "attach", id, force: args.includes("-f") });
638
- if (res.type === "session") {
639
- const result = await attach(res.session);
640
- handleAttachResult(result);
641
- } else if (res.type === "already-attached") {
642
- console.log(`Session ${id} is attached elsewhere.\n`);
643
- const choice = await select({
644
- message: "What to do?",
645
- choices: [
646
- { value: "force", name: "Take over" },
647
- { value: "cancel", name: "Cancel" },
648
- ]
649
- });
650
- if (choice === "force") {
651
- const res2 = await send({ type: "attach", id, force: true });
652
- if (res2.type === "session") {
653
- const result = await attach(res2.session);
654
- handleAttachResult(result);
655
- }
656
- }
657
- } else if (res.type === "error") {
658
- console.error("Error:", res.message);
659
- }
660
- } else {
661
- // No ID - show picker
662
- const res = await send({ type: "list" });
663
- if (res.type === "sessions") {
664
- if (res.sessions.length === 0) {
665
- console.log("No sessions. Create with: tl new");
666
- } else {
667
- const choice = await select({
668
- message: "Select session:",
669
- choices: res.sessions.map(s => ({
670
- value: s.id,
671
- name: formatSession(s)
672
- }))
673
- });
674
- const res2 = await send({ type: "attach", id: choice });
675
- if (res2.type === "session") {
676
- const result = await attach(res2.session);
677
- handleAttachResult(result);
678
- } else if (res2.type === "already-attached") {
679
- const force = await select({
680
- message: "Session attached. Take over?",
681
- choices: [
682
- { value: true, name: "Yes" },
683
- { value: false, name: "No" },
684
- ]
685
- });
686
- if (force) {
687
- const res3 = await send({ type: "attach", id: choice, force: true });
688
- if (res3.type === "session") {
689
- const result = await attach(res3.session);
690
- handleAttachResult(result);
691
- }
692
- }
693
- }
694
- }
695
- }
696
- }
697
- break;
698
- }
699
-
700
- case "ls":
701
- case "list": {
702
- const res = await send({ type: "list" });
703
- if (res.type === "sessions") {
704
- if (res.sessions.length === 0) {
705
- console.log("No sessions");
706
- } else {
707
- console.log("Sessions:");
708
- for (const s of res.sessions) {
709
- console.log(" " + formatSession(s));
710
- }
711
- }
712
- }
713
- break;
714
- }
715
-
716
- case "kill": {
717
- if (checkNested()) process.exit(1);
718
- const id = args[1];
719
- if (!id) {
720
- console.error("Usage: tl kill <id>");
721
- process.exit(1);
722
- }
723
- const res = await send({ type: "kill", id });
724
- if (res.type === "ok") {
725
- console.log(`Killed ${id}`);
726
- } else if (res.type === "error") {
727
- console.error("Error:", res.message);
728
- }
729
- break;
730
- }
731
-
732
- case "kill-server": {
733
- if (checkNested()) process.exit(1);
734
- await send({ type: "kill-server" });
735
- console.log("Server stopped");
736
- break;
737
- }
738
-
739
- case "inbox": {
740
- const res = await send({ type: "inbox" });
741
- if (res.type === "inbox") {
742
- if (res.items.length === 0) {
743
- console.log("Inbox empty");
744
- } else {
745
- console.log("Inbox:");
746
- for (const item of res.items) {
747
- const icon = item.type === 'exit'
748
- ? (item.exitCode === 0 ? '✓' : '✖')
749
- : '🔔';
750
- const status = item.read ? '' : ' (unread)';
751
- const time = new Date(item.timestamp).toLocaleTimeString();
752
- console.log(` ${icon} [${time}] ${item.sessionName}${status}`);
753
- // Indent context lines
754
- const lines = item.context.split('\n').slice(0, 3);
755
- for (const line of lines) {
756
- console.log(` ${line}`);
757
- }
758
- }
759
- }
760
- }
761
- break;
762
- }
763
-
764
- case "inbox-clear": {
765
- const id = args[1];
766
- await send({ type: "inbox-clear", id });
767
- console.log(id ? `Cleared inbox item ${id}` : "Inbox cleared");
768
- break;
769
- }
770
-
771
- default:
772
- console.log(`
773
- tmux-lite
774
-
775
- Commands:
776
- tl new [name] Create session
777
- tl attach [id] Attach (picker if no id)
778
- tl list List sessions
779
- tl kill <id> Kill session
780
- tl kill-server Stop server
781
- tl inbox Show inbox (bells, exits)
782
- tl inbox-clear Clear inbox
783
-
784
- In session:
785
- Ctrl+Esc Detach
786
- `);
787
- }
788
- }
789
-
790
- // Only run CLI when executed directly, not when imported as a module
791
- if (import.meta.main) {
792
- main().catch(e => {
793
- console.error(e.message);
794
- process.exit(1);
795
- });
796
- }