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,200 @@
1
+ /**
2
+ * List command implementation
3
+ * Handles 'gssh list' (workspaces), 'gssh list projects', and 'gssh list workspaces'
4
+ */
5
+
6
+ import { existsSync, readdirSync } from 'fs'
7
+ import { join } from 'path'
8
+ import {
9
+ getAllProjectNames,
10
+ readProjectConfig,
11
+ getCurrentProject,
12
+ getProjectWorkspacesDir,
13
+ readGlobalConfig,
14
+ } from '../core/config.js'
15
+ import { getWorktreeInfo } from '../core/git.js'
16
+ import { logger } from '../utils/logger.js'
17
+ import { SpacesError, NoProjectError } from '../types/errors.js'
18
+ import type { ProjectInfo, WorktreeInfo } from '../types/workspace.js'
19
+
20
+ /**
21
+ * List all projects
22
+ */
23
+ export async function listProjects(
24
+ options: {
25
+ json?: boolean
26
+ verbose?: boolean
27
+ } = {}
28
+ ): Promise<void> {
29
+ const projectNames = getAllProjectNames()
30
+
31
+ if (projectNames.length === 0) {
32
+ logger.info('No projects found')
33
+ logger.log('\nCreate a project:\n gssh add project')
34
+ return
35
+ }
36
+
37
+ const currentProject = getCurrentProject()
38
+ const projects: ProjectInfo[] = []
39
+
40
+ for (const name of projectNames) {
41
+ const config = readProjectConfig(name)
42
+ const workspacesDir = getProjectWorkspacesDir(name)
43
+
44
+ let workspaceCount = 0
45
+ if (existsSync(workspacesDir)) {
46
+ workspaceCount = readdirSync(workspacesDir).length
47
+ }
48
+
49
+ projects.push({
50
+ name,
51
+ repository: config.repository,
52
+ path: workspacesDir,
53
+ workspaceCount,
54
+ isCurrent: name === currentProject,
55
+ })
56
+ }
57
+
58
+ if (options.json) {
59
+ console.log(JSON.stringify(projects, null, 2))
60
+ return
61
+ }
62
+
63
+ logger.bold('Projects:')
64
+
65
+ for (const project of projects) {
66
+ const indicator = project.isCurrent ? ' *' : ' '
67
+ const currentLabel = project.isCurrent ? ' (current)' : ''
68
+
69
+ if (options.verbose) {
70
+ logger.log(
71
+ `${indicator} ${project.name.padEnd(20)} ${project.repository.padEnd(
72
+ 30
73
+ )} ${project.workspaceCount} workspaces${currentLabel}`
74
+ )
75
+ } else {
76
+ logger.log(
77
+ `${indicator} ${project.name.padEnd(20)} ${
78
+ project.repository
79
+ }${currentLabel}`
80
+ )
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Calculate days since last commit
87
+ */
88
+ function daysSinceCommit(date: Date): number {
89
+ const now = new Date()
90
+ const diff = now.getTime() - date.getTime()
91
+ return Math.floor(diff / (1000 * 60 * 60 * 24))
92
+ }
93
+
94
+ /**
95
+ * List workspaces in the current project
96
+ */
97
+ export async function listWorkspaces(
98
+ options: {
99
+ json?: boolean
100
+ verbose?: boolean
101
+ } = {}
102
+ ): Promise<void> {
103
+ const currentProject = getCurrentProject()
104
+ if (!currentProject) {
105
+ throw new NoProjectError()
106
+ }
107
+
108
+ const workspacesDir = getProjectWorkspacesDir(currentProject)
109
+
110
+ if (!existsSync(workspacesDir)) {
111
+ logger.info(`No workspaces found in project "${currentProject}"`)
112
+ logger.log('\nCreate a workspace:\n gssh add')
113
+ return
114
+ }
115
+
116
+ const workspaceNames = readdirSync(workspacesDir).filter((entry) => {
117
+ const path = join(workspacesDir, entry)
118
+ return existsSync(path)
119
+ })
120
+
121
+ if (workspaceNames.length === 0) {
122
+ logger.info(`No workspaces found in project "${currentProject}"`)
123
+ logger.log('\nCreate a workspace:\n gssh add')
124
+ return
125
+ }
126
+
127
+ // Get workspace info
128
+ const workspaces: WorktreeInfo[] = []
129
+ const globalConfig = readGlobalConfig()
130
+
131
+ for (const name of workspaceNames) {
132
+ const workspacePath = join(workspacesDir, name)
133
+ const info = await getWorktreeInfo(workspacePath)
134
+
135
+ if (info) {
136
+ workspaces.push(info)
137
+ }
138
+ }
139
+
140
+ if (options.json) {
141
+ console.log(JSON.stringify(workspaces, null, 2))
142
+ return
143
+ }
144
+
145
+ logger.bold(`Workspaces (${currentProject}):`)
146
+
147
+ for (const workspace of workspaces) {
148
+ const parts: string[] = []
149
+ parts.push(addSpace(2)) // indent
150
+ parts.push(truncateName(workspace.name, 40).padEnd(45)) // workspace name
151
+
152
+ // Branch and ahead/behind
153
+ if (workspace.ahead > 0 || workspace.behind > 0) {
154
+ parts.push(`+${workspace.ahead} -${workspace.behind}`.padEnd(10))
155
+ } else {
156
+ parts.push('+0 -0'.padEnd(10))
157
+ }
158
+
159
+ // Uncommitted changes
160
+ if (workspace.uncommittedChanges > 0) {
161
+ parts.push(`${workspace.uncommittedChanges} uncommitted`.padEnd(20))
162
+ } else {
163
+ parts.push('clean'.padEnd(20))
164
+ }
165
+
166
+ // Stale workspace warning
167
+ const daysSince = daysSinceCommit(workspace.lastCommitDate)
168
+ if (daysSince > globalConfig.staleDays) {
169
+ parts.push(`[stale: ${daysSince} days]`)
170
+ }
171
+
172
+ logger.log(parts.join(' '))
173
+
174
+ // Verbose mode: show last commit
175
+ if (options.verbose) {
176
+ logger.dim(` Last commit: ${workspace.lastCommit}`)
177
+ logger.dim(` Date: ${workspace.lastCommitDate.toLocaleDateString()}\n`)
178
+ }
179
+ }
180
+ }
181
+
182
+ function truncateName(
183
+ name: string,
184
+ maxLength: number,
185
+ includeEllipsis = true
186
+ ): string {
187
+ if (name.length <= maxLength) {
188
+ return name
189
+ }
190
+
191
+ if (includeEllipsis) {
192
+ return name.substring(0, maxLength - 3) + '...'
193
+ }
194
+
195
+ return name.substring(0, maxLength)
196
+ }
197
+
198
+ function addSpace(size: number): string {
199
+ return ' '.repeat(size)
200
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Relay command implementation
3
+ *
4
+ * Handles:
5
+ * - `gssh relay start` - Start the relay server
6
+ * - `gssh relay authorize` - Authorize a machine
7
+ * - `gssh relay revoke` - Revoke a machine's authorization
8
+ * - `gssh relay machines` - List authorized machines
9
+ * - `gssh relay trusted` - List trusted relays (machine-side)
10
+ * - `gssh relay untrust` - Remove relay trust (machine-side)
11
+ */
12
+
13
+ import { logger } from "../utils/logger.js";
14
+ import { createRelayServer } from "../relay/server.js";
15
+ import { SpacesError } from "../types/errors.js";
16
+ import chalk from "chalk";
17
+ import {
18
+ loadOrCreateRelayIdentity,
19
+ formatRelayFingerprint,
20
+ type RelayIdentity,
21
+ } from "../relay/identity.js";
22
+ import {
23
+ getAuthorizedMachines,
24
+ addAuthorizedMachine,
25
+ removeAuthorizedMachine,
26
+ computeMachineFingerprint,
27
+ type AuthorizedMachine,
28
+ } from "../relay/authorization.js";
29
+ import {
30
+ getTrustedRelays,
31
+ removeTrustedRelay,
32
+ type TrustedRelay,
33
+ } from "../core/trusted-relays.js";
34
+
35
+ /** Default port for relay server (4480 = "GIT0" on phone keypad) */
36
+ const DEFAULT_PORT = 4480;
37
+
38
+ /**
39
+ * Start the relay server
40
+ *
41
+ * @param options - Command options
42
+ */
43
+ export async function startRelay(options: {
44
+ port?: number;
45
+ hostname?: string;
46
+ bind?: string;
47
+ label?: string;
48
+ }): Promise<void> {
49
+ const port = options.port ?? parseInt(process.env.PORT ?? String(DEFAULT_PORT), 10);
50
+ const bind = options.bind ?? process.env.RELAY_BIND ?? "0.0.0.0";
51
+ const hostname = options.hostname ?? process.env.RELAY_HOST;
52
+
53
+ // Load or create relay identity
54
+ const identity = await loadOrCreateRelayIdentity(options.label);
55
+ const fingerprint = formatRelayFingerprint(identity.signingPublicKey);
56
+
57
+ // Display relay identity prominently
58
+ logger.log("");
59
+ logger.log(chalk.cyan("┌────────────────────────────────────────────────┐"));
60
+ logger.log(chalk.cyan("│") + chalk.bold(" Relay Identity ") + chalk.cyan("│"));
61
+ logger.log(chalk.cyan("│") + ` Fingerprint: ${chalk.yellow(fingerprint)} ` + chalk.cyan("│"));
62
+ if (identity.label) {
63
+ const labelPadded = identity.label.substring(0, 30).padEnd(30);
64
+ logger.log(chalk.cyan("│") + ` Label: ${chalk.dim(labelPadded)} ` + chalk.cyan("│"));
65
+ }
66
+ logger.log(chalk.cyan("│") + ` Public Key: ` + chalk.cyan("│"));
67
+ logger.log(chalk.cyan("│") + ` ${chalk.dim(identity.signingPublicKey.substring(0, 44))} ` + chalk.cyan("│"));
68
+ logger.log(chalk.cyan("└────────────────────────────────────────────────┘"));
69
+ logger.log("");
70
+
71
+ logger.log(` Port: ${port}`);
72
+ logger.log(` Bind: ${bind}`);
73
+ if (hostname) {
74
+ logger.log(` Hostname: ${hostname} (only serving this domain)`);
75
+ }
76
+ logger.log("");
77
+
78
+ try {
79
+ const server = await createRelayServer({
80
+ port,
81
+ bind,
82
+ hostname,
83
+ identity,
84
+ });
85
+
86
+ logger.success(`Relay listening on ws://${hostname || bind}:${port}`);
87
+ logger.log("");
88
+ logger.dim("Press Ctrl+C to stop");
89
+ logger.log("");
90
+
91
+ // Set up shutdown handlers
92
+ const shutdown = () => {
93
+ logger.log("");
94
+ logger.info("Shutting down relay...");
95
+ server.stop();
96
+ process.exit(0);
97
+ };
98
+
99
+ process.on("SIGINT", shutdown);
100
+ process.on("SIGTERM", shutdown);
101
+
102
+ // Keep process alive
103
+ await new Promise(() => {
104
+ // Never resolves
105
+ });
106
+ } catch (error) {
107
+ throw new SpacesError(
108
+ `Failed to start relay: ${error instanceof Error ? error.message : String(error)}`,
109
+ "SYSTEM_ERROR",
110
+ 2
111
+ );
112
+ }
113
+ }
114
+
115
+ // ============================================================================
116
+ // Authorization Commands
117
+ // ============================================================================
118
+
119
+ /**
120
+ * Authorize a machine to connect to this relay
121
+ *
122
+ * @param spacesPubKey - Machine public key in gssh-pub:SIGNING:KEYEXCHANGE format
123
+ * @param options - Command options
124
+ */
125
+ export async function authorizeMachine(
126
+ spacesPubKey: string,
127
+ options: { label?: string }
128
+ ): Promise<void> {
129
+ const entry = addAuthorizedMachine(spacesPubKey, options.label);
130
+
131
+ if (!entry) {
132
+ throw new SpacesError(
133
+ `Invalid public key format. Expected: gssh-pub:SIGNING_KEY:KEYEXCHANGE_KEY\n` +
134
+ `Get this from \`gssh identity show\` on the machine you want to authorize.`,
135
+ "USER_ERROR",
136
+ 1
137
+ );
138
+ }
139
+
140
+ logger.log("");
141
+ logger.success("Machine authorized!");
142
+ logger.log("");
143
+ logger.log(` Fingerprint: ${chalk.cyan(entry.fingerprint)}`);
144
+ if (entry.label) {
145
+ logger.log(` Label: ${chalk.yellow(entry.label)}`);
146
+ }
147
+ logger.log("");
148
+ }
149
+
150
+ /**
151
+ * Revoke a machine's authorization
152
+ *
153
+ * @param fingerprintOrLabel - Fingerprint or label of machine to revoke
154
+ */
155
+ export async function revokeMachine(fingerprintOrLabel: string): Promise<void> {
156
+ const removed = removeAuthorizedMachine(fingerprintOrLabel);
157
+
158
+ if (!removed) {
159
+ const machines = getAuthorizedMachines();
160
+
161
+ if (machines.length === 0) {
162
+ throw new SpacesError("No machines are authorized.", "USER_ERROR", 1);
163
+ }
164
+
165
+ logger.error(`No machine found matching: ${fingerprintOrLabel}`);
166
+ logger.log("");
167
+ logger.log("Authorized machines:");
168
+ for (const m of machines) {
169
+ logger.log(` ${m.fingerprint} ${m.label ? `(${m.label})` : ""}`);
170
+ }
171
+ throw new SpacesError("Machine not found.", "USER_ERROR", 1);
172
+ }
173
+
174
+ logger.log("");
175
+ logger.success("Machine authorization revoked.");
176
+ logger.log("");
177
+ logger.log(` Fingerprint: ${chalk.cyan(removed.fingerprint)}`);
178
+ if (removed.label) {
179
+ logger.log(` Label: ${chalk.yellow(removed.label)}`);
180
+ }
181
+ logger.log("");
182
+ }
183
+
184
+ /**
185
+ * List all authorized machines
186
+ */
187
+ export async function listMachines(): Promise<void> {
188
+ const machines = getAuthorizedMachines();
189
+
190
+ if (machines.length === 0) {
191
+ logger.log("");
192
+ logger.info("No machines authorized.");
193
+ logger.log("");
194
+ logger.log("Authorize a machine:");
195
+ logger.log(" gssh relay authorize gssh-pub:... --label 'My Machine'");
196
+ logger.log("");
197
+ return;
198
+ }
199
+
200
+ logger.log("");
201
+ logger.bold("Authorized Machines:");
202
+ logger.log("");
203
+
204
+ // Header
205
+ const fpWidth = 20;
206
+ const labelWidth = 24;
207
+ const dateWidth = 12;
208
+
209
+ logger.dim(
210
+ "FINGERPRINT".padEnd(fpWidth) +
211
+ "LABEL".padEnd(labelWidth) +
212
+ "AUTHORIZED"
213
+ );
214
+ logger.dim("─".repeat(fpWidth + labelWidth + dateWidth));
215
+
216
+ // Entries
217
+ for (const m of machines) {
218
+ const fp = m.fingerprint.padEnd(fpWidth);
219
+ const label = (m.label || "-").substring(0, labelWidth - 1).padEnd(labelWidth);
220
+ const date = new Date(m.authorizedAt).toISOString().split("T")[0];
221
+
222
+ logger.log(chalk.cyan(fp) + label + chalk.dim(date));
223
+ }
224
+
225
+ logger.log("");
226
+ logger.dim(`Total: ${machines.length} machine(s)`);
227
+ logger.log("");
228
+ }
229
+
230
+ // ============================================================================
231
+ // Trusted Relay Commands (Machine-side)
232
+ // ============================================================================
233
+
234
+ /**
235
+ * List all trusted relays
236
+ */
237
+ export async function listTrustedRelays(): Promise<void> {
238
+ const relays = getTrustedRelays();
239
+
240
+ if (relays.length === 0) {
241
+ logger.log("");
242
+ logger.info("No trusted relays.");
243
+ logger.log("");
244
+ logger.log("Connect to a relay to establish trust:");
245
+ logger.log(" gssh serve --relay wss://relay.example.com");
246
+ logger.log("");
247
+ return;
248
+ }
249
+
250
+ logger.log("");
251
+ logger.bold("Trusted Relays:");
252
+ logger.log("");
253
+
254
+ // Header
255
+ const urlWidth = 32;
256
+ const fpWidth = 20;
257
+ const labelWidth = 16;
258
+
259
+ logger.dim(
260
+ "URL".padEnd(urlWidth) +
261
+ "FINGERPRINT".padEnd(fpWidth) +
262
+ "LABEL"
263
+ );
264
+ logger.dim("─".repeat(urlWidth + fpWidth + labelWidth));
265
+
266
+ // Entries
267
+ for (const r of relays) {
268
+ const url = r.url.substring(0, urlWidth - 1).padEnd(urlWidth);
269
+ const fp = r.fingerprint.padEnd(fpWidth);
270
+ const label = (r.label || "-").substring(0, labelWidth - 1);
271
+
272
+ logger.log(chalk.cyan(url) + fp + chalk.dim(label));
273
+ }
274
+
275
+ logger.log("");
276
+ logger.dim(`Total: ${relays.length} relay(s)`);
277
+ logger.log("");
278
+ }
279
+
280
+ /**
281
+ * Remove trust for a relay
282
+ *
283
+ * @param urlOrFingerprint - URL, fingerprint, or label of relay to untrust
284
+ */
285
+ export async function untrustRelay(urlOrFingerprint: string): Promise<void> {
286
+ const removed = removeTrustedRelay(urlOrFingerprint);
287
+
288
+ if (!removed) {
289
+ const relays = getTrustedRelays();
290
+
291
+ if (relays.length === 0) {
292
+ throw new SpacesError("No relays are trusted.", "USER_ERROR", 1);
293
+ }
294
+
295
+ logger.error(`No relay found matching: ${urlOrFingerprint}`);
296
+ logger.log("");
297
+ logger.log("Trusted relays:");
298
+ for (const r of relays) {
299
+ logger.log(` ${r.url} (${r.fingerprint}${r.label ? `, ${r.label}` : ""})`);
300
+ }
301
+ throw new SpacesError("Relay not found.", "USER_ERROR", 1);
302
+ }
303
+
304
+ logger.log("");
305
+ logger.success("Relay trust removed.");
306
+ logger.log("");
307
+ logger.log(` URL: ${chalk.cyan(removed.url)}`);
308
+ logger.log(` Fingerprint: ${removed.fingerprint}`);
309
+ if (removed.label) {
310
+ logger.log(` Label: ${chalk.yellow(removed.label)}`);
311
+ }
312
+ logger.log("");
313
+ logger.dim("You will be prompted to trust this relay again on next connection.");
314
+ logger.log("");
315
+ }