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
package/src/index.ts ADDED
@@ -0,0 +1,821 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * GitSpace CLI (gssh) - Main entry point
5
+ * Manages GitHub workspaces with git worktrees and secure remote terminal access
6
+ */
7
+
8
+ import { Command } from 'commander'
9
+ import { readFileSync } from 'fs'
10
+ import { join } from 'path'
11
+ import { isFirstTimeSetup, initializeSpaces } from './core/config.js'
12
+ import { logger } from './utils/logger.js'
13
+ import { SpacesError } from './types/errors.js'
14
+ import { addProject, addWorkspace } from './commands/add.js'
15
+ import { switchProject, switchWorkspace } from './commands/switch.js'
16
+ import { listProjects, listWorkspaces } from './commands/list.js'
17
+ import { removeWorkspace, removeProject } from './commands/remove.js'
18
+ import { ensureDependencies } from './utils/deps.js'
19
+ import { getProjectDirectory } from './commands/directory.js'
20
+ import { launchTUI } from './tui/index.js'
21
+ import { addAccessKey, listAccessKeys, removeAccessKey } from './commands/access.js'
22
+ import { createShare } from './commands/share.js'
23
+ import { initIdentity, showIdentity } from './commands/identity.js'
24
+ import { connectToRemote } from './commands/connect.js'
25
+ import { serve, serveStart, serveStop, serveStatus } from './commands/serve.js'
26
+ import { startRelay, authorizeMachine, revokeMachine, listMachines, listTrustedRelays, untrustRelay } from './commands/relay.js'
27
+ import { authLogin, authLogout, authStatus } from './commands/auth.js'
28
+ import { hostReserve, hostRelease, hostList, hostSetPrimary, hostStatus } from './commands/host.js'
29
+ import { startTmux, stopTmux, statusTmux, listTmux, newTmux, attachTmux, killTmux } from './commands/tmux.js'
30
+ import { showStatus } from './commands/status.js'
31
+
32
+ const program = new Command()
33
+
34
+ // Read version from package.json
35
+ let version = '0.0.0'
36
+ try {
37
+ const pkgPath = join(import.meta.dir, '../package.json')
38
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
39
+ version = pkg.version
40
+ } catch {
41
+ // Fallback for compiled binary
42
+ }
43
+
44
+ // Package info
45
+ program
46
+ .name('gssh')
47
+ .description('GitSpace CLI - Manage GitHub workspaces with secure remote terminal access')
48
+ .version(version)
49
+
50
+ // First-time setup check
51
+ async function checkFirstTimeSetup(): Promise<void> {
52
+ if (isFirstTimeSetup()) {
53
+ logger.bold('Welcome to GitSpace CLI!\n')
54
+ logger.log('Initializing gitspace directory...\n')
55
+
56
+ // Check dependencies
57
+ try {
58
+ await ensureDependencies()
59
+ } catch (error) {
60
+ if (error instanceof SpacesError) {
61
+ logger.error(error.message)
62
+ process.exit(error.exitCode)
63
+ }
64
+ throw error
65
+ }
66
+
67
+ // Initialize spaces
68
+ initializeSpaces()
69
+
70
+ logger.success('GitSpace initialized!\n')
71
+ logger.log('Get started by adding a project:')
72
+ logger.command(' gssh add project\n')
73
+ }
74
+ }
75
+
76
+ // ============================================================================
77
+ // Add Commands
78
+ // ============================================================================
79
+
80
+ const addCommand = program
81
+ .command('add')
82
+ .description('Add a new project or workspace')
83
+
84
+ addCommand
85
+ .command('project')
86
+ .description('Add a new project from GitHub')
87
+ .option('--no-clone', 'Create project structure without cloning')
88
+ .option('--org <org>', 'Filter repos to specific organization')
89
+ .option('--linear-key <key>', 'Provide Linear API key via flag')
90
+ .option('--bundle-url <url>', 'Load bundle from remote URL (zip archive)')
91
+ .option('--bundle-path <path>', 'Load bundle from local directory')
92
+ .option('--skip-bundle', 'Skip bundle detection and onboarding')
93
+ .action(async (options) => {
94
+ await checkFirstTimeSetup()
95
+ try {
96
+ await addProject(options)
97
+ } catch (error) {
98
+ handleError(error)
99
+ }
100
+ })
101
+
102
+ addCommand
103
+ .argument('[workspace-name]', 'Name of the workspace to create')
104
+ .option(
105
+ '--branch <name>',
106
+ 'Specify different branch name from workspace name'
107
+ )
108
+ .option('--from <branch>', 'Create from specific branch instead of base')
109
+ .option('--no-shell', "Don't open interactive shell after creating workspace")
110
+ .option('--no-setup', 'Skip setup commands')
111
+ .action(async (workspaceName, options) => {
112
+ await checkFirstTimeSetup()
113
+ try {
114
+ await addWorkspace(workspaceName, options)
115
+ } catch (error) {
116
+ handleError(error)
117
+ }
118
+ })
119
+
120
+ // ============================================================================
121
+ // Switch Commands
122
+ // ============================================================================
123
+
124
+ const switchCommand = program
125
+ .command('switch')
126
+ .alias('sw')
127
+ .description('Switch to a different project or workspace')
128
+
129
+ switchCommand
130
+ .command('project')
131
+ .description('Switch to a different project')
132
+ .argument('[project-name]', 'Name of the project to switch to')
133
+ .action(async (projectName) => {
134
+ await checkFirstTimeSetup()
135
+ try {
136
+ await switchProject(projectName)
137
+ } catch (error) {
138
+ handleError(error)
139
+ }
140
+ })
141
+
142
+ switchCommand
143
+ .argument('[workspace-name]', 'Name of the workspace to switch to')
144
+ .option('--no-shell', "Don't open interactive shell, just print path")
145
+ .option('-f, --force', 'Jump to first fuzzy match without confirmation')
146
+ .action(async (workspaceName, options) => {
147
+ await checkFirstTimeSetup()
148
+ try {
149
+ await switchWorkspace(workspaceName, options)
150
+ } catch (error) {
151
+ handleError(error)
152
+ }
153
+ })
154
+
155
+ // ============================================================================
156
+ // List Commands
157
+ // ============================================================================
158
+
159
+ const listCommand = program
160
+ .command('list')
161
+ .alias('ls')
162
+ .description('List projects or workspaces')
163
+
164
+ listCommand
165
+ .command('projects')
166
+ .description('List all projects')
167
+ .option('--json', 'Output in JSON format')
168
+ .option('--verbose', 'Show additional details')
169
+ .action(async (options) => {
170
+ await checkFirstTimeSetup()
171
+ try {
172
+ await listProjects(options)
173
+ } catch (error) {
174
+ handleError(error)
175
+ }
176
+ })
177
+
178
+ listCommand
179
+ .command('workspaces')
180
+ .description('List workspaces in current project')
181
+ .option('--json', 'Output in JSON format')
182
+ .option('--verbose', 'Show additional details')
183
+ .action(async (options) => {
184
+ await checkFirstTimeSetup()
185
+ try {
186
+ await listWorkspaces(options)
187
+ } catch (error) {
188
+ handleError(error)
189
+ }
190
+ })
191
+
192
+ // Default list command (alias for list workspaces)
193
+ listCommand.action(async (options) => {
194
+ await checkFirstTimeSetup()
195
+ try {
196
+ await listWorkspaces(options)
197
+ } catch (error) {
198
+ handleError(error)
199
+ }
200
+ })
201
+
202
+ // ============================================================================
203
+ // Remove Commands
204
+ // ============================================================================
205
+
206
+ const removeCommand = program
207
+ .command('remove')
208
+ .alias('rm')
209
+ .description('Remove a workspace or project')
210
+
211
+ removeCommand
212
+ .command('workspace')
213
+ .description('Remove a workspace')
214
+ .argument('[workspace-name]', 'Name of the workspace to remove')
215
+ .option('--force', 'Skip confirmation prompts')
216
+ .option('--keep-branch', "Don't delete git branch when removing workspace")
217
+ .action(async (workspaceName, options) => {
218
+ await checkFirstTimeSetup()
219
+ try {
220
+ await removeWorkspace(workspaceName, options)
221
+ } catch (error) {
222
+ handleError(error)
223
+ }
224
+ })
225
+
226
+ removeCommand
227
+ .command('project')
228
+ .description('Remove a project')
229
+ .argument('[project-name]', 'Name of the project to remove')
230
+ .option('--force', 'Skip confirmation prompts')
231
+ .action(async (projectName, options) => {
232
+ await checkFirstTimeSetup()
233
+ try {
234
+ await removeProject(projectName, options)
235
+ } catch (error) {
236
+ handleError(error)
237
+ }
238
+ })
239
+
240
+ // Default remove command (alias for remove workspace)
241
+ removeCommand.action(async (options) => {
242
+ await checkFirstTimeSetup()
243
+ try {
244
+ await removeWorkspace(undefined, options)
245
+ } catch (error) {
246
+ handleError(error)
247
+ }
248
+ })
249
+
250
+ // ============================================================================
251
+ // Directory Commands
252
+ // ============================================================================
253
+
254
+ const directoryCommand = program
255
+ .command('directory')
256
+ .alias('dir')
257
+ .description('Manage directories')
258
+
259
+ directoryCommand.action(async (options) => {
260
+ await checkFirstTimeSetup()
261
+ try {
262
+ await getProjectDirectory(options)
263
+ } catch (error) {
264
+ handleError(error)
265
+ }
266
+ })
267
+
268
+ // ============================================================================
269
+ // Identity Commands
270
+ // ============================================================================
271
+
272
+ const identityCommand = program
273
+ .command('identity')
274
+ .description('Manage machine identity for secure remote connections')
275
+
276
+ identityCommand
277
+ .command('init')
278
+ .description('Initialize a new identity keypair')
279
+ .option('--force', 'Overwrite existing identity')
280
+ .action(async (options) => {
281
+ await checkFirstTimeSetup()
282
+ try {
283
+ await initIdentity(options)
284
+ } catch (error) {
285
+ handleError(error)
286
+ }
287
+ })
288
+
289
+ identityCommand
290
+ .command('show')
291
+ .description('Show identity information')
292
+ .option('--fingerprint', 'Show only fingerprint')
293
+ .option('--json', 'Output in JSON format')
294
+ .action(async (options) => {
295
+ await checkFirstTimeSetup()
296
+ try {
297
+ await showIdentity(options)
298
+ } catch (error) {
299
+ handleError(error)
300
+ }
301
+ })
302
+
303
+ // ============================================================================
304
+ // Access Commands
305
+ // ============================================================================
306
+
307
+ const accessCommand = program
308
+ .command('access')
309
+ .description('Manage access control for remote connections')
310
+
311
+ accessCommand
312
+ .command('add')
313
+ .description('Add a new access key (grants full access)')
314
+ .argument('<pubkey>', 'Public key (gssh-pub:SIGNING:KEYEXCHANGE or just SIGNING)')
315
+ .option('--label <name>', 'Human-readable label for this key')
316
+ .action(async (pubkey, options) => {
317
+ await checkFirstTimeSetup()
318
+ try {
319
+ await addAccessKey(pubkey, options)
320
+ } catch (error) {
321
+ handleError(error)
322
+ }
323
+ })
324
+
325
+ accessCommand
326
+ .command('list')
327
+ .alias('ls')
328
+ .description('List all access keys')
329
+ .option('--json', 'Output in JSON format')
330
+ .action(async (options) => {
331
+ await checkFirstTimeSetup()
332
+ try {
333
+ await listAccessKeys(options)
334
+ } catch (error) {
335
+ handleError(error)
336
+ }
337
+ })
338
+
339
+ accessCommand
340
+ .command('remove')
341
+ .alias('rm')
342
+ .description('Remove an access key')
343
+ .argument('<pubkey|label>', 'Public key, identity ID prefix, or label')
344
+ .option('--force', 'Skip confirmation prompt')
345
+ .action(async (pubkeyOrLabel, options) => {
346
+ await checkFirstTimeSetup()
347
+ try {
348
+ await removeAccessKey(pubkeyOrLabel, options)
349
+ } catch (error) {
350
+ handleError(error)
351
+ }
352
+ })
353
+
354
+ // ============================================================================
355
+ // Share Commands
356
+ // ============================================================================
357
+
358
+ const shareCommand = program
359
+ .command('share')
360
+ .description('Share workspace access via invite tokens')
361
+
362
+ shareCommand
363
+ .command('create')
364
+ .description('Create a share invite token (view-only session access)')
365
+ .option('--expires <duration>', 'Token validity duration (e.g., 1h, 24h, 7d, 1w)', '24h')
366
+ .option('--session <id>', 'Specific session ID to share (defaults to current session)')
367
+ .option('--relay <url>', 'Relay server URL', 'wss://relay.gitspace.sh')
368
+ .action(async (options) => {
369
+ await checkFirstTimeSetup()
370
+ try {
371
+ await createShare(options)
372
+ } catch (error) {
373
+ handleError(error)
374
+ }
375
+ })
376
+
377
+ // ============================================================================
378
+ // Connect Command
379
+ // ============================================================================
380
+
381
+ program
382
+ .command('connect')
383
+ .description('Connect to a remote machine via invite token')
384
+ .argument('[invite]', 'Invite token or URL (https://gitspace.sh/join#...)')
385
+ .option('--relay <url>', 'Override relay URL from invite token')
386
+ .action(async (invite, options) => {
387
+ await checkFirstTimeSetup()
388
+ try {
389
+ await connectToRemote(invite, options)
390
+ } catch (error) {
391
+ handleError(error)
392
+ }
393
+ })
394
+
395
+ // ============================================================================
396
+ // Serve Command
397
+ // ============================================================================
398
+
399
+ const serveCommand = program
400
+ .command('serve')
401
+ .description('Manage remote access daemon')
402
+
403
+ serveCommand
404
+ .command('start')
405
+ .description('Start the serve daemon')
406
+ .option('--relay <url>', 'Override default relay URL')
407
+ .option('--relay-pubkey <pubkey>', 'Relay public key for explicit trust (base64)')
408
+ .option('--password-stdin', 'Read password from stdin')
409
+ .option('--foreground', 'Run in foreground (don\'t daemonize)')
410
+ .action(async (options) => {
411
+ await checkFirstTimeSetup()
412
+ try {
413
+ await serveStart(options)
414
+ } catch (error) {
415
+ handleError(error)
416
+ }
417
+ })
418
+
419
+ serveCommand
420
+ .command('stop')
421
+ .description('Stop the serve daemon')
422
+ .action(async () => {
423
+ try {
424
+ await serveStop()
425
+ } catch (error) {
426
+ handleError(error)
427
+ }
428
+ })
429
+
430
+ serveCommand
431
+ .command('status')
432
+ .description('Show serve daemon status')
433
+ .action(async () => {
434
+ try {
435
+ await serveStatus()
436
+ } catch (error) {
437
+ handleError(error)
438
+ }
439
+ })
440
+
441
+ // Default action for 'gssh serve' (backwards compatibility - same as start)
442
+ serveCommand
443
+ .option('--relay <url>', 'Override default relay URL')
444
+ .option('--relay-pubkey <pubkey>', 'Relay public key for explicit trust (base64)')
445
+ .action(async (options) => {
446
+ await checkFirstTimeSetup()
447
+ try {
448
+ // Default to interactive (non-daemon) mode for backwards compatibility
449
+ await serve(options)
450
+ } catch (error) {
451
+ handleError(error)
452
+ }
453
+ })
454
+
455
+ // ============================================================================
456
+ // Relay Commands
457
+ // ============================================================================
458
+
459
+ const relayCommand = program
460
+ .command('relay')
461
+ .description('Manage relay server')
462
+
463
+ relayCommand
464
+ .command('start')
465
+ .description('Start the relay server')
466
+ .option('--port <port>', 'Port to listen on', '4480')
467
+ .option('--bind <address>', 'Address to bind to', '0.0.0.0')
468
+ .option('--hostname <host>', 'Only serve requests for this domain (optional)')
469
+ .option('--label <label>', 'Human-readable label for this relay')
470
+ .action(async (options) => {
471
+ try {
472
+ await startRelay({
473
+ port: parseInt(options.port, 10),
474
+ bind: options.bind,
475
+ hostname: options.hostname,
476
+ label: options.label,
477
+ })
478
+ } catch (error) {
479
+ handleError(error)
480
+ }
481
+ })
482
+
483
+ relayCommand
484
+ .command('authorize')
485
+ .description('Authorize a machine to connect to this relay')
486
+ .argument('<pubkey>', 'Machine public key in gssh-pub:SIGNING:KEYEXCHANGE format')
487
+ .option('--label <label>', 'Human-readable label for this machine')
488
+ .action(async (pubkey, options) => {
489
+ try {
490
+ await authorizeMachine(pubkey, { label: options.label })
491
+ } catch (error) {
492
+ handleError(error)
493
+ }
494
+ })
495
+
496
+ relayCommand
497
+ .command('revoke')
498
+ .description("Revoke a machine's authorization")
499
+ .argument('<fingerprint-or-label>', 'Fingerprint or label of machine to revoke')
500
+ .action(async (fingerprintOrLabel) => {
501
+ try {
502
+ await revokeMachine(fingerprintOrLabel)
503
+ } catch (error) {
504
+ handleError(error)
505
+ }
506
+ })
507
+
508
+ relayCommand
509
+ .command('machines')
510
+ .description('List authorized machines')
511
+ .action(async () => {
512
+ try {
513
+ await listMachines()
514
+ } catch (error) {
515
+ handleError(error)
516
+ }
517
+ })
518
+
519
+ relayCommand
520
+ .command('trusted')
521
+ .description('List trusted relays (machine-side)')
522
+ .action(async () => {
523
+ try {
524
+ await listTrustedRelays()
525
+ } catch (error) {
526
+ handleError(error)
527
+ }
528
+ })
529
+
530
+ relayCommand
531
+ .command('untrust')
532
+ .description('Remove trust for a relay (machine-side)')
533
+ .argument('<url-or-fingerprint>', 'URL, fingerprint, or label of relay to untrust')
534
+ .action(async (urlOrFingerprint) => {
535
+ try {
536
+ await untrustRelay(urlOrFingerprint)
537
+ } catch (error) {
538
+ handleError(error)
539
+ }
540
+ })
541
+
542
+
543
+ // ============================================================================
544
+ // Tmux Commands (tmux-lite daemon management)
545
+ // ============================================================================
546
+
547
+ const tmuxCommand = program
548
+ .command('tmux')
549
+ .description('Manage tmux-lite terminal session daemon')
550
+
551
+ tmuxCommand
552
+ .command('start')
553
+ .description('Start the tmux-lite server daemon')
554
+ .action(async () => {
555
+ try {
556
+ await startTmux()
557
+ } catch (error) {
558
+ handleError(error)
559
+ }
560
+ })
561
+
562
+ tmuxCommand
563
+ .command('stop')
564
+ .description('Stop the tmux-lite server daemon')
565
+ .option('--force', 'Stop even if sessions are active')
566
+ .action(async (options) => {
567
+ try {
568
+ await stopTmux({ force: options.force })
569
+ } catch (error) {
570
+ handleError(error)
571
+ }
572
+ })
573
+
574
+ tmuxCommand
575
+ .command('status')
576
+ .description('Show tmux-lite server status')
577
+ .action(async () => {
578
+ try {
579
+ await statusTmux()
580
+ } catch (error) {
581
+ handleError(error)
582
+ }
583
+ })
584
+
585
+ tmuxCommand
586
+ .command('list')
587
+ .description('List active tmux-lite sessions')
588
+ .action(async () => {
589
+ try {
590
+ await listTmux()
591
+ } catch (error) {
592
+ handleError(error)
593
+ }
594
+ })
595
+
596
+ tmuxCommand
597
+ .command('new [name]')
598
+ .description('Create and attach to a new session')
599
+ .action(async (name) => {
600
+ try {
601
+ await newTmux(name)
602
+ } catch (error) {
603
+ handleError(error)
604
+ }
605
+ })
606
+
607
+ tmuxCommand
608
+ .command('attach <id>')
609
+ .description('Attach to a session (by id or name)')
610
+ .option('--force', 'Take over if attached elsewhere')
611
+ .action(async (id, options) => {
612
+ try {
613
+ await attachTmux(id, { force: options.force })
614
+ } catch (error) {
615
+ handleError(error)
616
+ }
617
+ })
618
+
619
+ tmuxCommand
620
+ .command('kill <id>')
621
+ .description('Kill a session (by id or name)')
622
+ .action(async (id) => {
623
+ try {
624
+ await killTmux(id)
625
+ } catch (error) {
626
+ handleError(error)
627
+ }
628
+ })
629
+
630
+ // ============================================================================
631
+ // Auth Commands (gitspace.sh)
632
+ // ============================================================================
633
+
634
+ const authCommand = program
635
+ .command('auth')
636
+ .description('Manage gitspace.sh authentication')
637
+
638
+ authCommand
639
+ .command('login')
640
+ .description('Login with GitHub')
641
+ .action(async () => {
642
+ await checkFirstTimeSetup()
643
+ try {
644
+ await authLogin()
645
+ } catch (error) {
646
+ handleError(error)
647
+ }
648
+ })
649
+
650
+ authCommand
651
+ .command('logout')
652
+ .description('Logout and clear credentials')
653
+ .action(async () => {
654
+ try {
655
+ await authLogout()
656
+ } catch (error) {
657
+ handleError(error)
658
+ }
659
+ })
660
+
661
+ authCommand
662
+ .command('status')
663
+ .description('Show login status')
664
+ .action(async () => {
665
+ try {
666
+ await authStatus()
667
+ } catch (error) {
668
+ handleError(error)
669
+ }
670
+ })
671
+
672
+ // ============================================================================
673
+ // Host Commands (gitspace.sh hosting)
674
+ // ============================================================================
675
+
676
+ const hostCommand = program
677
+ .command('host')
678
+ .description('Manage gitspace.sh hosting')
679
+
680
+ hostCommand
681
+ .command('reserve <subdomain>')
682
+ .description('Reserve a subdomain (e.g., brad.gitspace.sh)')
683
+ .action(async (subdomain) => {
684
+ await checkFirstTimeSetup()
685
+ try {
686
+ await hostReserve(subdomain)
687
+ } catch (error) {
688
+ handleError(error)
689
+ }
690
+ })
691
+
692
+ hostCommand
693
+ .command('release [subdomain]')
694
+ .description('Release a subdomain')
695
+ .action(async (subdomain) => {
696
+ try {
697
+ await hostRelease(subdomain)
698
+ } catch (error) {
699
+ handleError(error)
700
+ }
701
+ })
702
+
703
+ hostCommand
704
+ .command('list')
705
+ .alias('ls')
706
+ .description('List your subdomains')
707
+ .action(async () => {
708
+ try {
709
+ await hostList()
710
+ } catch (error) {
711
+ handleError(error)
712
+ }
713
+ })
714
+
715
+ hostCommand
716
+ .command('set-primary <subdomain>')
717
+ .description('Set primary subdomain for `gssh serve`')
718
+ .action(async (subdomain) => {
719
+ try {
720
+ await hostSetPrimary(subdomain)
721
+ } catch (error) {
722
+ handleError(error)
723
+ }
724
+ })
725
+
726
+ hostCommand
727
+ .command('status')
728
+ .description('Show hosting status')
729
+ .action(async () => {
730
+ try {
731
+ await hostStatus()
732
+ } catch (error) {
733
+ handleError(error)
734
+ }
735
+ })
736
+
737
+ // ============================================================================
738
+ // Status Command (unified daemon status)
739
+ // ============================================================================
740
+
741
+ program
742
+ .command('status')
743
+ .description('Show status of all spaces daemons')
744
+ .action(async () => {
745
+ try {
746
+ await showStatus()
747
+ } catch (error) {
748
+ handleError(error)
749
+ }
750
+ })
751
+
752
+ // ============================================================================
753
+ // Error Handling
754
+ // ============================================================================
755
+
756
+ function handleError(error: unknown): never {
757
+ if (error instanceof SpacesError) {
758
+ logger.error(error.message)
759
+ process.exit(error.exitCode)
760
+ }
761
+
762
+ if (error instanceof Error) {
763
+ logger.error(`Unexpected error: ${error.message}`)
764
+ logger.debug(error.stack || '')
765
+ process.exit(1)
766
+ }
767
+
768
+ logger.error('An unexpected error occurred')
769
+ process.exit(1)
770
+ }
771
+
772
+ // ============================================================================
773
+ // Parse and Execute
774
+ // ============================================================================
775
+
776
+ // Handle uncaught errors
777
+ process.on('uncaughtException', (error) => {
778
+ logger.error(`Uncaught exception: ${error.message}`)
779
+ logger.debug(error.stack || '')
780
+ process.exit(1)
781
+ })
782
+
783
+ process.on('unhandledRejection', (reason) => {
784
+ logger.error(`Unhandled rejection: ${reason}`)
785
+ process.exit(1)
786
+ })
787
+
788
+ // Parse command line arguments
789
+ // Check for global relay options (TUI mode with relay)
790
+ const args = process.argv.slice(2)
791
+ const hasRelayOption = args.includes('--relay')
792
+ const hasOnlyRelayOptions = args.every(arg =>
793
+ arg === '--relay' ||
794
+ (args[args.indexOf('--relay') + 1] === arg)
795
+ )
796
+
797
+ // If no args provided or only relay options, launch TUI
798
+ if (process.argv.length === 2 || (hasRelayOption && hasOnlyRelayOptions)) {
799
+ // Extract options manually instead of using commander.parse() which shows help
800
+ const relayIndex = args.indexOf('--relay')
801
+ const relayUrl = relayIndex >= 0 ? args[relayIndex + 1] : undefined
802
+
803
+ // Build relay config if provided (auth now via challenge-response, not token)
804
+ const relayConfig = relayUrl ? {
805
+ url: relayUrl,
806
+ } : undefined
807
+
808
+ // Launch TUI
809
+ checkFirstTimeSetup()
810
+ .then(() => launchTUI(relayConfig))
811
+ .catch((error) => {
812
+ if (error instanceof SpacesError) {
813
+ logger.error(error.message)
814
+ process.exit(error.exitCode)
815
+ }
816
+ logger.error(`Failed to launch TUI: ${error instanceof Error ? error.message : 'Unknown error'}`)
817
+ process.exit(1)
818
+ })
819
+ } else {
820
+ program.parse()
821
+ }