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,510 @@
1
+ /**
2
+ * Configuration management for global and project configs
3
+ */
4
+
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ writeFileSync,
10
+ readdirSync,
11
+ statSync,
12
+ chmodSync,
13
+ } from 'fs'
14
+ import { join, dirname } from 'path'
15
+ import { homedir } from 'os'
16
+ import type { GlobalConfig, ProjectConfig } from '../types/config.js'
17
+ import {
18
+ DEFAULT_GLOBAL_CONFIG,
19
+ createDefaultProjectConfig,
20
+ } from '../types/config.js'
21
+ import { SpacesError } from '../types/errors.js'
22
+
23
+ /**
24
+ * Get the global gitspace directory path
25
+ */
26
+ export function getGitspaceDir(): string {
27
+ return join(homedir(), 'gitspace')
28
+ }
29
+
30
+ /**
31
+ * @deprecated Use getGitspaceDir() instead
32
+ */
33
+ export function getSpacesDir(): string {
34
+ return getGitspaceDir()
35
+ }
36
+
37
+ /**
38
+ * Get the global config file path
39
+ */
40
+ export function getGlobalConfigPath(): string {
41
+ return join(getSpacesDir(), '.config.json')
42
+ }
43
+
44
+ /**
45
+ * Get a project directory path
46
+ */
47
+ export function getProjectDir(projectName: string): string {
48
+ return join(getSpacesDir(), projectName)
49
+ }
50
+
51
+ /**
52
+ * Get a project config file path
53
+ */
54
+ export function getProjectConfigPath(projectName: string): string {
55
+ return join(getProjectDir(projectName), '.config.json')
56
+ }
57
+
58
+ /**
59
+ * Get the base repository directory for a project
60
+ */
61
+ export function getProjectBaseDir(projectName: string): string {
62
+ return join(getProjectDir(projectName), 'base')
63
+ }
64
+
65
+ /**
66
+ * Get the workspaces directory for a project
67
+ */
68
+ export function getProjectWorkspacesDir(projectName: string): string {
69
+ return join(getProjectDir(projectName), 'workspaces')
70
+ }
71
+
72
+ /**
73
+ * Get the scripts directory for a project
74
+ */
75
+ export function getProjectScriptsDir(projectName: string): string {
76
+ return join(getProjectDir(projectName), 'scripts')
77
+ }
78
+
79
+ /**
80
+ * Get a specific scripts phase directory (pre, setup, select)
81
+ */
82
+ export function getScriptsPhaseDir(
83
+ projectName: string,
84
+ phase: 'pre' | 'setup' | 'select' | 'remove'
85
+ ): string {
86
+ return join(getProjectScriptsDir(projectName), phase)
87
+ }
88
+
89
+ /**
90
+ * Initialize global config with defaults
91
+ */
92
+ function initializeGlobalConfig(): GlobalConfig {
93
+ return {
94
+ ...DEFAULT_GLOBAL_CONFIG,
95
+ projectsDir: getSpacesDir(),
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Read global configuration
101
+ */
102
+ export function readGlobalConfig(): GlobalConfig {
103
+ const configPath = getGlobalConfigPath()
104
+
105
+ if (!existsSync(configPath)) {
106
+ // Return default config if file doesn't exist
107
+ return initializeGlobalConfig()
108
+ }
109
+
110
+ try {
111
+ const content = readFileSync(configPath, 'utf-8')
112
+ const config = JSON.parse(content) as GlobalConfig
113
+
114
+ // Merge with defaults to ensure all fields exist
115
+ return {
116
+ ...initializeGlobalConfig(),
117
+ ...config,
118
+ }
119
+ } catch (error) {
120
+ throw new SpacesError(
121
+ `Failed to read global config: ${
122
+ error instanceof Error ? error.message : 'Unknown error'
123
+ }`,
124
+ 'SYSTEM_ERROR',
125
+ 2
126
+ )
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Write global configuration
132
+ */
133
+ export function writeGlobalConfig(config: GlobalConfig): void {
134
+ const configPath = getGlobalConfigPath()
135
+ const spacesDir = dirname(configPath)
136
+
137
+ // Ensure spaces directory exists
138
+ if (!existsSync(spacesDir)) {
139
+ mkdirSync(spacesDir, { recursive: true })
140
+ }
141
+
142
+ try {
143
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
144
+ chmodSync(configPath, 0o600)
145
+ } catch (error) {
146
+ throw new SpacesError(
147
+ `Failed to write global config: ${
148
+ error instanceof Error ? error.message : 'Unknown error'
149
+ }`,
150
+ 'SYSTEM_ERROR',
151
+ 2
152
+ )
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Update global configuration
158
+ */
159
+ export function updateGlobalConfig(
160
+ updates: Partial<GlobalConfig>
161
+ ): GlobalConfig {
162
+ const config = readGlobalConfig()
163
+ const updated = { ...config, ...updates }
164
+ writeGlobalConfig(updated)
165
+ return updated
166
+ }
167
+
168
+ /**
169
+ * Read project configuration
170
+ */
171
+ export function readProjectConfig(projectName: string): ProjectConfig {
172
+ const configPath = getProjectConfigPath(projectName)
173
+
174
+ if (!existsSync(configPath)) {
175
+ throw new SpacesError(`Project "${projectName}" not found`, 'USER_ERROR', 1)
176
+ }
177
+
178
+ try {
179
+ const content = readFileSync(configPath, 'utf-8')
180
+ return JSON.parse(content) as ProjectConfig
181
+ } catch (error) {
182
+ throw new SpacesError(
183
+ `Failed to read project config for "${projectName}": ${
184
+ error instanceof Error ? error.message : 'Unknown error'
185
+ }`,
186
+ 'SYSTEM_ERROR',
187
+ 2
188
+ )
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Write project configuration
194
+ */
195
+ export function writeProjectConfig(
196
+ projectName: string,
197
+ config: ProjectConfig
198
+ ): void {
199
+ const configPath = getProjectConfigPath(projectName)
200
+ const projectDir = dirname(configPath)
201
+
202
+ // Ensure project directory exists
203
+ if (!existsSync(projectDir)) {
204
+ mkdirSync(projectDir, { recursive: true })
205
+ }
206
+
207
+ try {
208
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
209
+ chmodSync(configPath, 0o600)
210
+ } catch (error) {
211
+ throw new SpacesError(
212
+ `Failed to write project config for "${projectName}": ${
213
+ error instanceof Error ? error.message : 'Unknown error'
214
+ }`,
215
+ 'SYSTEM_ERROR',
216
+ 2
217
+ )
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Update project configuration
223
+ */
224
+ export function updateProjectConfig(
225
+ projectName: string,
226
+ updates: Partial<ProjectConfig>
227
+ ): ProjectConfig {
228
+ const config = readProjectConfig(projectName)
229
+ const updated = { ...config, ...updates }
230
+ writeProjectConfig(projectName, updated)
231
+ return updated
232
+ }
233
+
234
+ /**
235
+ * Get current project name from env var or global config
236
+ * Resolution order:
237
+ * 1. SPACES_CURRENT_PROJECT environment variable
238
+ * 2. currentProject field in global config
239
+ * 3. null if neither is set
240
+ */
241
+ export function getCurrentProject(): string | null {
242
+ // Check environment variable first
243
+ const envProject = process.env.SPACES_CURRENT_PROJECT
244
+ if (envProject) {
245
+ return envProject
246
+ }
247
+
248
+ // Fall back to global config
249
+ const globalConfig = readGlobalConfig()
250
+ return globalConfig.currentProject
251
+ }
252
+
253
+ /**
254
+ * Set current project in global config
255
+ */
256
+ export function setCurrentProject(projectName: string): void {
257
+ updateGlobalConfig({ currentProject: projectName })
258
+ }
259
+
260
+ /**
261
+ * Check if the global config exists (first-time setup check)
262
+ */
263
+ export function isFirstTimeSetup(): boolean {
264
+ return !existsSync(getGlobalConfigPath())
265
+ }
266
+
267
+ /**
268
+ * Initialize spaces directory and config for first-time setup
269
+ */
270
+ export function initializeSpaces(): void {
271
+ const spacesDir = getSpacesDir()
272
+
273
+ // Create spaces directory if it doesn't exist
274
+ if (!existsSync(spacesDir)) {
275
+ mkdirSync(spacesDir, { recursive: true })
276
+ }
277
+
278
+ // Create global config if it doesn't exist
279
+ if (!existsSync(getGlobalConfigPath())) {
280
+ writeGlobalConfig(initializeGlobalConfig())
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Get all project names
286
+ */
287
+ export function getAllProjectNames(): string[] {
288
+ const spacesDir = getSpacesDir()
289
+
290
+ if (!existsSync(spacesDir)) {
291
+ return []
292
+ }
293
+
294
+ try {
295
+ const entries = readdirSync(spacesDir) as string[]
296
+
297
+ // Filter to only directories that have a .config.json file
298
+ return entries.filter((entry: string) => {
299
+ const projectDir = join(spacesDir, entry)
300
+ const configPath = join(projectDir, '.config.json')
301
+ return (
302
+ statSync(projectDir).isDirectory() &&
303
+ existsSync(configPath) &&
304
+ entry !== 'app' // Exclude the app directory
305
+ )
306
+ })
307
+ } catch (error) {
308
+ throw new SpacesError(
309
+ `Failed to list projects: ${
310
+ error instanceof Error ? error.message : 'Unknown error'
311
+ }`,
312
+ 'SYSTEM_ERROR',
313
+ 2
314
+ )
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Check if a project exists
320
+ */
321
+ export function projectExists(projectName: string): boolean {
322
+ const configPath = getProjectConfigPath(projectName)
323
+ return existsSync(configPath)
324
+ }
325
+
326
+ /**
327
+ * Create a new project configuration
328
+ */
329
+ export function createProject(
330
+ projectName: string,
331
+ repository: string,
332
+ baseBranch: string,
333
+ linearApiKey?: string,
334
+ linearTeamKey?: string
335
+ ): ProjectConfig {
336
+ const config = createDefaultProjectConfig(
337
+ projectName,
338
+ repository,
339
+ baseBranch,
340
+ linearApiKey,
341
+ linearTeamKey
342
+ )
343
+
344
+ // Create project directories
345
+ const projectDir = getProjectDir(projectName)
346
+ const baseDir = getProjectBaseDir(projectName)
347
+ const workspacesDir = getProjectWorkspacesDir(projectName)
348
+
349
+ mkdirSync(projectDir, { recursive: true })
350
+ mkdirSync(baseDir, { recursive: true })
351
+ mkdirSync(workspacesDir, { recursive: true })
352
+
353
+ // Create scripts directories
354
+ mkdirSync(getScriptsPhaseDir(projectName, 'pre'), { recursive: true })
355
+ mkdirSync(getScriptsPhaseDir(projectName, 'setup'), { recursive: true })
356
+ mkdirSync(getScriptsPhaseDir(projectName, 'select'), { recursive: true })
357
+ mkdirSync(getScriptsPhaseDir(projectName, 'remove'), { recursive: true })
358
+
359
+ // Create example template scripts in each phase directory
360
+ const preExampleScript = `#!/bin/bash
361
+ # Pre-phase script - runs BEFORE workspace shell opens
362
+ #
363
+ # Current working directory: ~/gitspace/<project>/workspaces/<workspace>/
364
+ # (Scripts run from the workspace directory, so you can use relative paths)
365
+ #
366
+ # This script runs in your terminal immediately after the worktree is created.
367
+ # Perfect for preparation tasks like:
368
+ # - Copying environment files (cp .env.example .env)
369
+ # - Creating directories (mkdir -p tmp/uploads)
370
+ # - Any setup that other scripts might need
371
+ #
372
+ # Arguments:
373
+ # $1 - Workspace name (e.g., "my-feature")
374
+ # $2 - Repository name (e.g., "myorg/my-app")
375
+ #
376
+ # To use this script:
377
+ # 1. Rename it (e.g., 01-copy-env.sh)
378
+ # 2. Add your commands
379
+ # 3. Make it executable: chmod +x scripts/pre/01-copy-env.sh
380
+
381
+ WORKSPACE_NAME=$1
382
+ REPOSITORY=$2
383
+
384
+ echo "Running Spaces pre-install on: $WORKSPACE_NAME from $REPOSITORY"
385
+ `
386
+
387
+ const setupExampleScript = `#!/bin/bash
388
+ # Setup-phase script - runs ONCE when workspace is first created
389
+ #
390
+ # Current working directory: ~/gitspace/<project>/workspaces/<workspace>/
391
+ # (Scripts run from the workspace directory, so you can use relative paths)
392
+ #
393
+ # This script runs the first time a workspace is created.
394
+ # Perfect for one-time setup tasks like:
395
+ # - Installing dependencies (npm install, bundle install)
396
+ # - Initial builds (npm run build)
397
+ # - Database setup
398
+ # - Any expensive setup that should only run once
399
+ #
400
+ # After all setup scripts run, GitSpace creates a gitspace.lock marker file
401
+ # to prevent them from running again.
402
+ #
403
+ # Arguments:
404
+ # $1 - Workspace name (e.g., "my-feature")
405
+ # $2 - Repository name (e.g., "myorg/my-app")
406
+ #
407
+ # To use this script:
408
+ # 1. Rename it (e.g., 01-install.sh)
409
+ # 2. Add your commands
410
+ # 3. Make it executable: chmod +x scripts/setup/01-install.sh
411
+
412
+ WORKSPACE_NAME=$1
413
+ REPOSITORY=$2
414
+
415
+ echo "Setting up Spaces workspace on: $WORKSPACE_NAME from $REPOSITORY"
416
+ `
417
+
418
+ const selectExampleScript = `#!/bin/bash
419
+ # Select-phase script - runs EVERY TIME you switch to a workspace
420
+ #
421
+ # Current working directory: ~/gitspace/<project>/workspaces/<workspace>/
422
+ # (Scripts run from the workspace directory, so you can use relative paths)
423
+ #
424
+ # This script runs every time you switch to an existing workspace
425
+ # (where setup already completed).
426
+ # Perfect for quick status updates like:
427
+ # - Fetching latest changes (git fetch --all)
428
+ # - Checking workspace state (git status)
429
+ # - Environment checks
430
+ # - Quick status updates
431
+ #
432
+ # Arguments:
433
+ # $1 - Workspace name (e.g., "my-feature")
434
+ # $2 - Repository name (e.g., "myorg/my-app")
435
+ #
436
+ # To use this script:
437
+ # 1. Rename it (e.g., 01-fetch.sh)
438
+ # 2. Add your commands
439
+ # 3. Make it executable: chmod +x scripts/select/01-fetch.sh
440
+
441
+ WORKSPACE_NAME=$1
442
+ REPOSITORY=$2
443
+
444
+ echo "Running Spaces script on: $WORKSPACE_NAME from $REPOSITORY"
445
+ `
446
+
447
+ const removeExampleScript = `#!/bin/bash
448
+ # Remove-phase script - runs when workspace is REMOVED
449
+ #
450
+ # Current working directory: ~/gitspace/<project>/workspaces/<workspace>/
451
+ # (Scripts run from the workspace directory, so you can use relative paths)
452
+ #
453
+ # This script runs in your terminal when you remove a workspace,
454
+ # BEFORE the worktree is deleted. Perfect for cleanup tasks like:
455
+ # - Tearing down test databases
456
+ # - Removing cloud resources (S3 buckets, EC2 instances)
457
+ # - Cleaning up external services
458
+ # - Removing temporary Docker containers/volumes
459
+ # - Cleaning up API keys or tokens
460
+ #
461
+ # Arguments:
462
+ # $1 - Workspace name (e.g., "my-feature")
463
+ # $2 - Repository name (e.g., "myorg/my-app")
464
+ #
465
+ # To use this script:
466
+ # 1. Rename it (e.g., 01-cleanup-db.sh)
467
+ # 2. Add your commands
468
+ # 3. Make it executable: chmod +x scripts/remove/01-cleanup-db.sh
469
+
470
+ WORKSPACE_NAME=$1
471
+ REPOSITORY=$2
472
+
473
+ echo "Running Spaces cleanup on: $WORKSPACE_NAME from $REPOSITORY"
474
+ `
475
+
476
+ // Write and make executable
477
+ const preExamplePath = join(
478
+ getScriptsPhaseDir(projectName, 'pre'),
479
+ '00-example.sh'
480
+ )
481
+ const setupExamplePath = join(
482
+ getScriptsPhaseDir(projectName, 'setup'),
483
+ '00-example.sh'
484
+ )
485
+ const selectExamplePath = join(
486
+ getScriptsPhaseDir(projectName, 'select'),
487
+ '00-example.sh'
488
+ )
489
+ const removeExamplePath = join(
490
+ getScriptsPhaseDir(projectName, 'remove'),
491
+ '00-example.sh'
492
+ )
493
+
494
+ writeFileSync(preExamplePath, preExampleScript, 'utf-8')
495
+ chmodSync(preExamplePath, 0o755)
496
+
497
+ writeFileSync(setupExamplePath, setupExampleScript, 'utf-8')
498
+ chmodSync(setupExamplePath, 0o755)
499
+
500
+ writeFileSync(selectExamplePath, selectExampleScript, 'utf-8')
501
+ chmodSync(selectExamplePath, 0o755)
502
+
503
+ writeFileSync(removeExamplePath, removeExampleScript, 'utf-8')
504
+ chmodSync(removeExamplePath, 0o755)
505
+
506
+ // Write project config
507
+ writeProjectConfig(projectName, config)
508
+
509
+ return config
510
+ }