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,452 @@
1
+ /**
2
+ * Add command implementation
3
+ * Handles both 'gssh add project' and 'gssh add [workspace-name]'
4
+ */
5
+
6
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import {
9
+ readProjectConfig,
10
+ createProject,
11
+ setCurrentProject,
12
+ getProjectBaseDir,
13
+ getProjectWorkspacesDir,
14
+ getCurrentProject,
15
+ getAllProjectNames,
16
+ projectExists,
17
+ getScriptsPhaseDir,
18
+ updateProjectConfig,
19
+ } from '../core/config.js';
20
+ import { checkGitHubAuth, ensureDependencies } from '../utils/deps.js';
21
+ import { selectItem, promptConfirm, promptPassword, promptInput } from '../utils/prompts.js';
22
+ import { logger } from '../utils/logger.js';
23
+ import { listAllRepos, cloneRepository } from '../core/github.js';
24
+ import {
25
+ getDefaultBranch,
26
+ createWorktree,
27
+ checkRemoteBranch,
28
+ listRemoteBranches,
29
+ } from '../core/git.js';
30
+ import { openWorkspaceShell } from '../core/shell.js';
31
+ import { fetchUnstartedIssues } from '../core/linear.js';
32
+ import {
33
+ sanitizeForFileSystem,
34
+ generateWorkspaceName,
35
+ isValidWorkspaceName,
36
+ extractRepoName,
37
+ } from '../utils/sanitize.js';
38
+ import {
39
+ SpacesError,
40
+ NoProjectError,
41
+ ProjectExistsError,
42
+ WorkspaceExistsError,
43
+ } from '../types/errors.js';
44
+ import type { CreateWorkspaceOptions } from '../types/workspace.js';
45
+ import { runScriptsInTerminal } from '../utils/run-scripts.js';
46
+ import { hasSetupBeenRun } from '../utils/workspace-state.js';
47
+ import { generateMarkdown } from '../utils/markdown.js';
48
+ import {
49
+ detectBundleInRepo,
50
+ loadBundleFromPath,
51
+ loadBundleFromUrl,
52
+ copyBundleScripts,
53
+ cleanupBundleDir,
54
+ } from '../core/bundle.js';
55
+ import { runOnboarding } from '../utils/onboarding.js';
56
+ import type { LoadedBundle, OnboardingResult } from '../types/bundle.js';
57
+
58
+ /**
59
+ * Add a new project
60
+ */
61
+ export async function addProject(options: {
62
+ noClone?: boolean;
63
+ org?: string;
64
+ linearKey?: string;
65
+ bundleUrl?: string;
66
+ bundlePath?: string;
67
+ skipBundle?: boolean;
68
+ }): Promise<void> {
69
+ // Check dependencies
70
+ await ensureDependencies();
71
+ await checkGitHubAuth();
72
+
73
+ // List all GitHub repositories
74
+ logger.info('Fetching repositories...');
75
+ const repos = await listAllRepos(options.org);
76
+
77
+ if (repos.length === 0) {
78
+ throw new SpacesError(
79
+ 'No repositories found',
80
+ 'USER_ERROR',
81
+ 1
82
+ );
83
+ }
84
+
85
+ // Select repository
86
+ const selectedRepo = await selectItem(repos, 'Select a repository:');
87
+
88
+ if (!selectedRepo) {
89
+ logger.info('Cancelled');
90
+ return;
91
+ }
92
+
93
+ logger.success(`Selected: ${selectedRepo}`);
94
+
95
+ // Extract repo name for project directory
96
+ const projectName = extractRepoName(selectedRepo);
97
+
98
+ // Check if project already exists
99
+ if (projectExists(projectName)) {
100
+ throw new ProjectExistsError(
101
+ projectName,
102
+ getProjectBaseDir(projectName)
103
+ );
104
+ }
105
+
106
+ // Check for duplicate repositories
107
+ const existingProjects = getAllProjectNames();
108
+ for (const existingProject of existingProjects) {
109
+ const existingConfig = readProjectConfig(existingProject);
110
+ if (existingConfig.repository === selectedRepo) {
111
+ throw new SpacesError(
112
+ `Repository ${selectedRepo} is already tracked by project "${existingProject}"\n\nTo use that project:\n gssh switch project ${existingProject}`,
113
+ 'USER_ERROR',
114
+ 1
115
+ );
116
+ }
117
+ }
118
+
119
+ // Clone the repository unless --no-clone
120
+ const baseDir = getProjectBaseDir(projectName);
121
+
122
+ if (!options.noClone) {
123
+ logger.info(`Cloning to ${baseDir}...`);
124
+ await cloneRepository(selectedRepo, baseDir);
125
+ logger.success(`Cloned to ${baseDir}`);
126
+ }
127
+
128
+ // Detect default branch
129
+ const baseBranch = await getDefaultBranch(baseDir);
130
+ logger.debug(`Detected default branch: ${baseBranch}`);
131
+
132
+ // Handle bundle detection and loading
133
+ let loadedBundle: LoadedBundle | null = null;
134
+ let onboardingResult: OnboardingResult | null = null;
135
+
136
+ if (!options.skipBundle) {
137
+ if (options.bundleUrl) {
138
+ // Load from explicit URL
139
+ loadedBundle = await loadBundleFromUrl(options.bundleUrl);
140
+ } else if (options.bundlePath) {
141
+ // Load from explicit local path
142
+ loadedBundle = loadBundleFromPath(options.bundlePath);
143
+ } else if (!options.noClone) {
144
+ // Detect bundle in cloned repo
145
+ const bundleDir = detectBundleInRepo(baseDir);
146
+ if (bundleDir) {
147
+ loadedBundle = loadBundleFromPath(bundleDir);
148
+ logger.info(`Detected spaces bundle: ${loadedBundle.bundle.name}`);
149
+ }
150
+ }
151
+
152
+ // Run onboarding if bundle has steps
153
+ if (loadedBundle?.bundle.onboarding && loadedBundle.bundle.onboarding.length > 0) {
154
+ const proceed = await promptConfirm(
155
+ `This repository has ${loadedBundle.bundle.onboarding.length} onboarding step(s). Run them now?`,
156
+ true
157
+ );
158
+
159
+ if (proceed) {
160
+ onboardingResult = await runOnboarding(loadedBundle.bundle.onboarding);
161
+
162
+ if (!onboardingResult.completed) {
163
+ const continueAnyway = await promptConfirm(
164
+ 'Continue creating project without completing onboarding?',
165
+ false
166
+ );
167
+ if (!continueAnyway) {
168
+ // Clean up bundle temp dir if from URL
169
+ if (loadedBundle) {
170
+ cleanupBundleDir(loadedBundle.bundleDir);
171
+ }
172
+ logger.info('Cancelled');
173
+ return;
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // Ask about Linear integration
181
+ const useLinear = await promptConfirm('Does this project use Linear?', false);
182
+
183
+ let linearApiKey: string | undefined;
184
+ let linearTeamKey: string | undefined;
185
+
186
+ if (useLinear) {
187
+ if (!options.linearKey) {
188
+ linearApiKey = await promptPassword('Enter Linear API key:') || undefined;
189
+ } else {
190
+ linearApiKey = options.linearKey;
191
+ }
192
+
193
+ linearTeamKey = await promptInput('Enter Linear team key (optional, e.g., ENG):') || undefined;
194
+ }
195
+
196
+ // Create project configuration
197
+ createProject(
198
+ projectName,
199
+ selectedRepo,
200
+ baseBranch,
201
+ linearApiKey,
202
+ linearTeamKey
203
+ );
204
+
205
+ // Copy bundle scripts if bundle was loaded
206
+ if (loadedBundle) {
207
+ copyBundleScripts(loadedBundle.bundleDir, projectName);
208
+
209
+ // Store bundle values and info in project config
210
+ const configUpdates: Record<string, unknown> = {};
211
+
212
+ if (onboardingResult?.completed && Object.keys(onboardingResult.configValues).length > 0) {
213
+ configUpdates.bundleValues = onboardingResult.configValues;
214
+ }
215
+
216
+ configUpdates.appliedBundle = {
217
+ name: loadedBundle.bundle.name,
218
+ version: loadedBundle.bundle.version,
219
+ source: loadedBundle.source,
220
+ appliedAt: new Date().toISOString(),
221
+ };
222
+
223
+ updateProjectConfig(projectName, configUpdates);
224
+
225
+ // Clean up temp directory if bundle was from URL
226
+ cleanupBundleDir(loadedBundle.bundleDir);
227
+ }
228
+
229
+ logger.success(`Project '${projectName}' created`);
230
+
231
+ // Set as current project
232
+ setCurrentProject(projectName);
233
+ logger.success('Set as current project');
234
+ }
235
+
236
+ /**
237
+ * Add a new workspace
238
+ */
239
+ export async function addWorkspace(
240
+ workspaceNameArg?: string,
241
+ options: Partial<CreateWorkspaceOptions> = {}
242
+ ): Promise<void> {
243
+ // Get current project
244
+ const currentProject = getCurrentProject();
245
+ if (!currentProject) {
246
+ throw new NoProjectError();
247
+ }
248
+
249
+ const projectConfig = readProjectConfig(currentProject);
250
+ const baseDir = getProjectBaseDir(currentProject);
251
+ const workspacesDir = getProjectWorkspacesDir(currentProject);
252
+
253
+ let workspaceName: string;
254
+ let branchName: string;
255
+
256
+ let existsRemotely = false;
257
+ let selectedLinearIssue: Awaited<ReturnType<typeof fetchUnstartedIssues>>[0] | undefined;
258
+
259
+ if (workspaceNameArg) {
260
+ // Workspace name provided as argument
261
+ if (!isValidWorkspaceName(workspaceNameArg)) {
262
+ throw new SpacesError(
263
+ `Invalid workspace name: ${workspaceNameArg}\nWorkspace names must contain only alphanumeric characters, hyphens, and underscores (no spaces).`,
264
+ 'USER_ERROR',
265
+ 1
266
+ );
267
+ }
268
+
269
+ workspaceName = workspaceNameArg;
270
+ branchName = options.branchName || workspaceName;
271
+ } else {
272
+ // No workspace name provided, prompt for source
273
+ const sourceOptions = ['Create from GitHub branch', 'Create with manual name'];
274
+
275
+ // Add Linear option if configured
276
+ if (projectConfig.linearApiKey) {
277
+ sourceOptions.splice(1, 0, 'Create from Linear issue');
278
+ }
279
+
280
+ const source = await selectItem(sourceOptions, 'How would you like to create the workspace?');
281
+
282
+ if (!source) {
283
+ logger.info('Cancelled');
284
+ return;
285
+ }
286
+
287
+ if (source === 'Create from GitHub branch') {
288
+ // List remote branches
289
+ logger.info('Fetching remote branches...');
290
+ const allBranches = await listRemoteBranches(baseDir);
291
+
292
+ // Filter out the base branch
293
+ const branches = allBranches.filter((branch) => branch !== projectConfig.baseBranch);
294
+
295
+ if (branches.length === 0) {
296
+ throw new SpacesError(
297
+ `No remote branches found (excluding base branch ${projectConfig.baseBranch})`,
298
+ 'USER_ERROR',
299
+ 1
300
+ );
301
+ }
302
+
303
+ const selectedBranch = await selectItem(branches, 'Select a branch:');
304
+
305
+ if (!selectedBranch) {
306
+ logger.info('Cancelled');
307
+ return;
308
+ }
309
+
310
+ // Use branch name as workspace name (sanitize for filesystem safety)
311
+ workspaceName = sanitizeForFileSystem(selectedBranch);
312
+ branchName = selectedBranch;
313
+ existsRemotely = true; // We know it exists remotely
314
+ } else if (source === 'Create from Linear issue') {
315
+ // Fetch unstarted issues from Linear
316
+ logger.info('Fetching Linear issues...');
317
+
318
+ const issues = await fetchUnstartedIssues(
319
+ projectConfig.linearApiKey!,
320
+ projectConfig.linearTeamKey
321
+ );
322
+
323
+ if (issues.length === 0) {
324
+ throw new SpacesError(
325
+ 'No unstarted Linear issues found',
326
+ 'USER_ERROR',
327
+ 1
328
+ );
329
+ }
330
+
331
+ // Format for selection
332
+ const issueOptions = issues.map(
333
+ (issue) => `${issue.identifier} - ${issue.title}`
334
+ );
335
+
336
+ const selectedIssueString = await selectItem(issueOptions, 'Select an issue:');
337
+
338
+ if (!selectedIssueString) {
339
+ logger.info('Cancelled');
340
+ return;
341
+ }
342
+
343
+ // Find the corresponding LinearIssue object
344
+ const [identifier] = selectedIssueString.split(' - ');
345
+ selectedLinearIssue = issues.find(issue => issue.identifier === identifier);
346
+
347
+ if (!selectedLinearIssue) {
348
+ throw new SpacesError(
349
+ `Failed to find Linear issue with identifier ${identifier}`,
350
+ 'SYSTEM_ERROR',
351
+ 2
352
+ );
353
+ }
354
+
355
+ // Generate workspace name
356
+ workspaceName = generateWorkspaceName(selectedLinearIssue.identifier, selectedLinearIssue.title);
357
+ branchName = options.branchName || workspaceName;
358
+ } else {
359
+ // Manual entry
360
+ const name = await promptInput('Enter workspace name:', {
361
+ validate: (input) => {
362
+ if (!input || input.trim().length === 0) {
363
+ return 'Workspace name is required';
364
+ }
365
+ if (!isValidWorkspaceName(input)) {
366
+ return 'Workspace name must contain only alphanumeric characters, hyphens, and underscores (no spaces)';
367
+ }
368
+ return true;
369
+ },
370
+ });
371
+
372
+ if (!name) {
373
+ logger.info('Cancelled');
374
+ return;
375
+ }
376
+
377
+ workspaceName = name;
378
+ branchName = options.branchName || workspaceName;
379
+ }
380
+ }
381
+
382
+ const workspacePath = join(workspacesDir, workspaceName);
383
+
384
+ // Check if workspace already exists
385
+ if (existsSync(workspacePath)) {
386
+ throw new WorkspaceExistsError(workspaceName);
387
+ }
388
+
389
+ logger.info(`Creating workspace: ${workspaceName}`);
390
+
391
+ // Check if branch exists remotely (if we don't already know)
392
+ if (!existsRemotely) {
393
+ existsRemotely = await checkRemoteBranch(baseDir, branchName);
394
+
395
+ if (existsRemotely) {
396
+ // Prompt user
397
+ const pullRemote = await promptConfirm(`Branch '${branchName}' exists on remote. Pull it down?`, true);
398
+
399
+ if (!pullRemote) {
400
+ logger.info('Cancelled');
401
+ return;
402
+ }
403
+ }
404
+ }
405
+
406
+ // Create worktree
407
+ const baseBranch = options.fromBranch || projectConfig.baseBranch;
408
+ await createWorktree(
409
+ baseDir,
410
+ workspacePath,
411
+ branchName,
412
+ baseBranch,
413
+ existsRemotely
414
+ );
415
+
416
+ logger.success(`Created worktree from ${baseBranch}`);
417
+
418
+ // If workspace was created from a Linear issue, save issue details as markdown
419
+ if (selectedLinearIssue) {
420
+ const promptDir = join(workspacePath, '.prompt');
421
+ mkdirSync(promptDir, { recursive: true });
422
+
423
+ const markdown = await generateMarkdown(selectedLinearIssue, promptDir, projectConfig.linearApiKey);
424
+ const issueMarkdownPath = join(promptDir, 'issue.md');
425
+ writeFileSync(issueMarkdownPath, markdown, 'utf-8');
426
+
427
+ logger.debug('Saved Linear issue details to .prompt/issue.md');
428
+ }
429
+
430
+ // Check if this is first-time setup (no marker exists)
431
+ const isFirstTime = !hasSetupBeenRun(workspacePath);
432
+
433
+ // Run pre scripts if this is the first time (before tmux/setup)
434
+ if (isFirstTime && !options.noSetup) {
435
+ const preScriptsDir = getScriptsPhaseDir(currentProject, 'pre');
436
+ await runScriptsInTerminal(preScriptsDir, workspacePath, workspaceName, projectConfig.repository);
437
+ }
438
+
439
+ // Open workspace shell unless --no-shell
440
+ if (!options.noShell) {
441
+ logger.success(`Opening workspace: ${workspaceName}`);
442
+ await openWorkspaceShell(
443
+ workspacePath,
444
+ currentProject,
445
+ projectConfig.repository,
446
+ options.noSetup || false
447
+ );
448
+ } else {
449
+ logger.success(`Workspace created at: ${workspacePath}`);
450
+ logger.log(`\nTo navigate:\n cd ${workspacePath}`);
451
+ }
452
+ }