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,317 @@
1
+ /**
2
+ * Git and worktree operations
3
+ */
4
+
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import { existsSync } from 'fs';
8
+ import { SpacesError } from '../types/errors.js';
9
+ import { logger } from '../utils/logger.js';
10
+ import { escapeShellArg } from '../utils/shell-escape.js';
11
+ import type { WorktreeInfo } from '../types/workspace.js';
12
+
13
+ const execAsync = promisify(exec);
14
+
15
+ /**
16
+ * Get the default branch of a repository
17
+ */
18
+ export async function getDefaultBranch(repoPath: string): Promise<string> {
19
+ try {
20
+ const { stdout } = await execAsync(
21
+ 'git symbolic-ref refs/remotes/origin/HEAD',
22
+ { cwd: repoPath }
23
+ );
24
+
25
+ // Extract branch name from refs/remotes/origin/main -> main
26
+ const branch = stdout.trim().replace('refs/remotes/origin/', '');
27
+ return branch;
28
+ } catch (error) {
29
+ // Fallback to 'main' if we can't determine
30
+ logger.debug(`Could not determine default branch, using 'main': ${error}`);
31
+ return 'main';
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Check if a branch exists on remote
37
+ */
38
+ export async function checkRemoteBranch(
39
+ repoPath: string,
40
+ branchName: string
41
+ ): Promise<boolean> {
42
+ try {
43
+ await execAsync(
44
+ `git ls-remote --exit-code --heads origin ${escapeShellArg(branchName)}`,
45
+ { cwd: repoPath }
46
+ );
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * List all remote branches from origin
55
+ * @param repoPath Path to the git repository
56
+ * @returns Array of branch names (without origin/ prefix)
57
+ */
58
+ export async function listRemoteBranches(repoPath: string): Promise<string[]> {
59
+ try {
60
+ // Fetch latest from remote
61
+ await execAsync('git fetch --all --prune', { cwd: repoPath });
62
+
63
+ const { stdout } = await execAsync(
64
+ 'git ls-remote --heads origin',
65
+ { cwd: repoPath }
66
+ );
67
+
68
+ // Parse output: "hash\trefs/heads/branch-name"
69
+ const branches = stdout
70
+ .trim()
71
+ .split('\n')
72
+ .filter((line) => line.length > 0)
73
+ .map((line) => {
74
+ // Extract branch name from "hash\trefs/heads/branch-name"
75
+ const match = line.match(/refs\/heads\/(.+)$/);
76
+ return match ? match[1] : null;
77
+ })
78
+ .filter((branch): branch is string => branch !== null);
79
+
80
+ return branches;
81
+ } catch (error) {
82
+ throw new SpacesError(
83
+ `Failed to list remote branches: ${error instanceof Error ? error.message : 'Unknown error'}`,
84
+ 'SYSTEM_ERROR',
85
+ 2
86
+ );
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Check if a branch exists locally
92
+ */
93
+ export async function checkLocalBranch(
94
+ repoPath: string,
95
+ branchName: string
96
+ ): Promise<boolean> {
97
+ try {
98
+ await execAsync(
99
+ `git show-ref --verify --quiet ${escapeShellArg(`refs/heads/${branchName}`)}`,
100
+ { cwd: repoPath }
101
+ );
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Create a git worktree
110
+ */
111
+ export async function createWorktree(
112
+ repoPath: string,
113
+ workspacePath: string,
114
+ branchName: string,
115
+ baseBranch: string,
116
+ existsRemotely?: boolean
117
+ ): Promise<void> {
118
+ try {
119
+ // Check if worktree path already exists
120
+ if (existsSync(workspacePath)) {
121
+ throw new SpacesError(
122
+ `Worktree path already exists: ${workspacePath}`,
123
+ 'USER_ERROR',
124
+ 1
125
+ );
126
+ }
127
+
128
+ // Fetch latest changes
129
+ logger.debug('Fetching latest changes...');
130
+ await execAsync('git fetch --all --prune', { cwd: repoPath });
131
+
132
+ // Pull latest base branch
133
+ try {
134
+ await execAsync(`git pull --ff-only origin ${escapeShellArg(baseBranch)}`, {
135
+ cwd: repoPath,
136
+ });
137
+ } catch (error) {
138
+ logger.debug(`Could not fast-forward ${baseBranch}: ${error}`);
139
+ }
140
+
141
+ // Determine how to create the worktree
142
+ if (existsRemotely) {
143
+ // Branch exists on remote, create from remote branch
144
+ logger.debug(`Creating worktree from remote branch: ${branchName}`);
145
+ await execAsync(
146
+ `git worktree add ${escapeShellArg(workspacePath)} -b ${escapeShellArg(branchName)} ${escapeShellArg(`origin/${branchName}`)}`,
147
+ { cwd: repoPath }
148
+ );
149
+ } else if (await checkLocalBranch(repoPath, branchName)) {
150
+ // Branch exists locally, attach worktree to it
151
+ logger.debug(`Creating worktree from local branch: ${branchName}`);
152
+ await execAsync(`git worktree add ${escapeShellArg(workspacePath)} ${escapeShellArg(branchName)}`, {
153
+ cwd: repoPath,
154
+ });
155
+ } else {
156
+ // Branch doesn't exist, create new from base
157
+ // Use --no-track to avoid setting upstream to baseBranch (user should push -u to set correct upstream)
158
+ logger.debug(`Creating new branch from ${baseBranch}: ${branchName}`);
159
+ await execAsync(
160
+ `git worktree add -b ${escapeShellArg(branchName)} ${escapeShellArg(workspacePath)} ${escapeShellArg(`origin/${baseBranch}`)} --no-track`,
161
+ { cwd: repoPath }
162
+ );
163
+ }
164
+ } catch (error) {
165
+ if (error instanceof SpacesError) {
166
+ throw error;
167
+ }
168
+
169
+ throw new SpacesError(
170
+ `Failed to create worktree: ${error instanceof Error ? error.message : 'Unknown error'}`,
171
+ 'SYSTEM_ERROR',
172
+ 2
173
+ );
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Remove a git worktree
179
+ */
180
+ export async function removeWorktree(
181
+ repoPath: string,
182
+ workspacePath: string,
183
+ force: boolean = false
184
+ ): Promise<void> {
185
+ try {
186
+ const forceFlag = force ? '--force' : '';
187
+ await execAsync(`git worktree remove ${escapeShellArg(workspacePath)} ${forceFlag}`, {
188
+ cwd: repoPath,
189
+ });
190
+ } catch (error) {
191
+ throw new SpacesError(
192
+ `Failed to remove worktree: ${error instanceof Error ? error.message : 'Unknown error'}`,
193
+ 'SYSTEM_ERROR',
194
+ 2
195
+ );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Get information about a worktree
201
+ */
202
+ export async function getWorktreeInfo(workspacePath: string): Promise<WorktreeInfo | null> {
203
+ try {
204
+ if (!existsSync(workspacePath)) {
205
+ return null;
206
+ }
207
+
208
+ // Get current branch
209
+ const { stdout: branchOutput } = await execAsync(
210
+ 'git rev-parse --abbrev-ref HEAD',
211
+ { cwd: workspacePath }
212
+ );
213
+ const branch = branchOutput.trim();
214
+
215
+ // Get commits ahead/behind
216
+ let ahead = 0;
217
+ let behind = 0;
218
+ try {
219
+ const { stdout: revListOutput } = await execAsync(
220
+ `git rev-list --left-right --count ${escapeShellArg(`HEAD...origin/${branch}`)}`,
221
+ { cwd: workspacePath }
222
+ );
223
+ const [aheadStr, behindStr] = revListOutput.trim().split('\t');
224
+ ahead = parseInt(aheadStr, 10) || 0;
225
+ behind = parseInt(behindStr, 10) || 0;
226
+ } catch {
227
+ // Branch may not have remote tracking
228
+ logger.debug(`Could not get ahead/behind for ${branch}`);
229
+ }
230
+
231
+ // Get uncommitted changes count
232
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', {
233
+ cwd: workspacePath,
234
+ });
235
+ const uncommittedChanges = statusOutput
236
+ .trim()
237
+ .split('\n')
238
+ .filter((line) => line.length > 0).length;
239
+
240
+ // Get last commit info
241
+ const { stdout: lastCommitMsg } = await execAsync(
242
+ 'git log -1 --pretty=format:"%s"',
243
+ { cwd: workspacePath }
244
+ );
245
+ const { stdout: lastCommitDate } = await execAsync(
246
+ 'git log -1 --pretty=format:"%aI"',
247
+ { cwd: workspacePath }
248
+ );
249
+
250
+ const name = workspacePath.split('/').pop() || '';
251
+
252
+ return {
253
+ name,
254
+ path: workspacePath,
255
+ branch,
256
+ ahead,
257
+ behind,
258
+ uncommittedChanges,
259
+ lastCommit: lastCommitMsg.trim() || 'No commits',
260
+ lastCommitDate: lastCommitDate ? new Date(lastCommitDate) : new Date(),
261
+ };
262
+ } catch (error) {
263
+ logger.debug(`Failed to get worktree info for ${workspacePath}: ${error}`);
264
+ return null;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Delete a local branch
270
+ */
271
+ export async function deleteLocalBranch(
272
+ repoPath: string,
273
+ branchName: string,
274
+ force: boolean = false
275
+ ): Promise<void> {
276
+ try {
277
+ const forceFlag = force ? '-D' : '-d';
278
+ await execAsync(`git branch ${forceFlag} ${escapeShellArg(branchName)}`, {
279
+ cwd: repoPath,
280
+ });
281
+ } catch (error) {
282
+ throw new SpacesError(
283
+ `Failed to delete branch ${branchName}: ${error instanceof Error ? error.message : 'Unknown error'}`,
284
+ 'SYSTEM_ERROR',
285
+ 2
286
+ );
287
+ }
288
+ }
289
+
290
+ /**
291
+ * List all worktrees in a repository
292
+ */
293
+ export async function listWorktrees(repoPath: string): Promise<string[]> {
294
+ try {
295
+ const { stdout } = await execAsync('git worktree list --porcelain', {
296
+ cwd: repoPath,
297
+ });
298
+
299
+ const worktrees: string[] = [];
300
+ const lines = stdout.trim().split('\n');
301
+
302
+ for (const line of lines) {
303
+ if (line.startsWith('worktree ')) {
304
+ const path = line.replace('worktree ', '');
305
+ worktrees.push(path);
306
+ }
307
+ }
308
+
309
+ return worktrees;
310
+ } catch (error) {
311
+ throw new SpacesError(
312
+ `Failed to list worktrees: ${error instanceof Error ? error.message : 'Unknown error'}`,
313
+ 'SYSTEM_ERROR',
314
+ 2
315
+ );
316
+ }
317
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * GitHub repository operations using gh CLI
3
+ */
4
+
5
+ import { exec } from 'child_process'
6
+ import { promisify } from 'util'
7
+ import { SpacesError } from '../types/errors.js'
8
+ import { logger } from '../utils/logger.js'
9
+
10
+ const execAsync = promisify(exec)
11
+
12
+ /**
13
+ * Get current GitHub user login
14
+ */
15
+ async function getCurrentUser(): Promise<string> {
16
+ try {
17
+ const { stdout } = await execAsync('gh api user --jq .login')
18
+ return stdout.trim()
19
+ } catch (error) {
20
+ throw new SpacesError(
21
+ `Failed to get GitHub user: ${
22
+ error instanceof Error ? error.message : 'Unknown error'
23
+ }`,
24
+ 'SERVICE_ERROR',
25
+ 3
26
+ )
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get all organizations the user belongs to
32
+ */
33
+ async function getUserOrgs(): Promise<string[]> {
34
+ try {
35
+ const { stdout } = await execAsync(
36
+ 'gh api user/orgs --paginate --jq ".[].login"'
37
+ )
38
+ const orgs = stdout
39
+ .trim()
40
+ .split('\n')
41
+ .filter((org) => org.length > 0)
42
+ return orgs
43
+ } catch (error) {
44
+ // If no orgs, that's okay
45
+ return []
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get repositories for a specific owner (user or org)
51
+ */
52
+ async function getReposForOwner(
53
+ owner: string,
54
+ limit: number = 1000
55
+ ): Promise<string[]> {
56
+ try {
57
+ const { stdout } = await execAsync(
58
+ `gh repo list "${owner}" --limit ${limit} --json 'name,owner' | jq -r '.[] | "\\(.owner.login)/\\(.name)"'`
59
+ )
60
+
61
+ const repos = stdout
62
+ .trim()
63
+ .split('\n')
64
+ .filter((repo) => repo.length > 0)
65
+
66
+ return repos
67
+ } catch (error) {
68
+ logger.debug(
69
+ `Failed to get repos for ${owner}: ${
70
+ error instanceof Error ? error.message : 'Unknown error'
71
+ }`
72
+ )
73
+ return []
74
+ }
75
+ }
76
+
77
+ /**
78
+ * List all accessible GitHub repositories
79
+ */
80
+ export async function listAllRepos(orgFilter?: string): Promise<string[]> {
81
+ try {
82
+ const allRepos: string[] = []
83
+
84
+ if (orgFilter) {
85
+ // Only fetch repos for the specified org
86
+ const repos = await getReposForOwner(orgFilter)
87
+ allRepos.push(...repos)
88
+ } else {
89
+ // Get current user
90
+ const currentUser = await getCurrentUser()
91
+
92
+ // Get user's repos
93
+ const userRepos = await getReposForOwner(currentUser)
94
+ allRepos.push(...userRepos)
95
+
96
+ // Get orgs and their repos
97
+ const orgs = await getUserOrgs()
98
+ for (const org of orgs) {
99
+ const orgRepos = await getReposForOwner(org)
100
+ allRepos.push(...orgRepos)
101
+ }
102
+ }
103
+
104
+ // Remove duplicates and sort
105
+ const uniqueRepos = Array.from(new Set(allRepos))
106
+ uniqueRepos.sort()
107
+
108
+ return uniqueRepos
109
+ } catch (error) {
110
+ if (error instanceof SpacesError) {
111
+ throw error
112
+ }
113
+
114
+ throw new SpacesError(
115
+ `Failed to list GitHub repositories: ${
116
+ error instanceof Error ? error.message : 'Unknown error'
117
+ }`,
118
+ 'SERVICE_ERROR',
119
+ 3
120
+ )
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Clone a repository
126
+ */
127
+ export async function cloneRepository(
128
+ repository: string,
129
+ destination: string
130
+ ): Promise<void> {
131
+ try {
132
+ logger.debug(`Cloning ${repository} to ${destination}`)
133
+
134
+ const { stdout, stderr } = await execAsync(
135
+ `gh repo clone ${repository} "${destination}"`
136
+ )
137
+
138
+ logger.debug(stdout)
139
+ if (stderr) {
140
+ logger.debug(stderr)
141
+ }
142
+ } catch (error) {
143
+ throw new SpacesError(
144
+ `Failed to clone repository ${repository}: ${
145
+ error instanceof Error ? error.message : 'Unknown error'
146
+ }`,
147
+ 'SYSTEM_ERROR',
148
+ 2
149
+ )
150
+ }
151
+ }