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.
- package/.claude/settings.local.json +21 -0
- package/.gitspace/bundle.json +50 -0
- package/.gitspace/select/01-status.sh +40 -0
- package/.gitspace/setup/01-install-deps.sh +12 -0
- package/.gitspace/setup/02-typecheck.sh +16 -0
- package/AGENTS.md +439 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +25 -0
- package/README.md +607 -0
- package/bin/gssh +62 -0
- package/bun.lock +647 -0
- package/docs/CONNECTION.md +623 -0
- package/docs/GATEWAY-WORKER.md +319 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GITSPACE-PLATFORM.md +1819 -0
- package/docs/INFRASTRUCTURE.md +1347 -0
- package/docs/PROTOCOL.md +619 -0
- package/docs/QUICKSTART.md +174 -0
- package/docs/RELAY.md +327 -0
- package/docs/REMOTE-DESIGN.md +549 -0
- package/docs/ROADMAP.md +564 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
- package/docs/STACK-DESIGN.md +588 -0
- package/docs/UNIFIED_ARCHITECTURE.md +292 -0
- package/experiments/pty-benchmark.ts +148 -0
- package/experiments/pty-latency.ts +100 -0
- package/experiments/router/client.ts +199 -0
- package/experiments/router/protocol.ts +74 -0
- package/experiments/router/router.ts +217 -0
- package/experiments/router/session.ts +180 -0
- package/experiments/router/test.ts +133 -0
- package/experiments/socket-bandwidth.ts +77 -0
- package/homebrew/gitspace.rb +45 -0
- package/landing-page/ATTRIBUTIONS.md +3 -0
- package/landing-page/README.md +11 -0
- package/landing-page/bun.lock +801 -0
- package/landing-page/guidelines/Guidelines.md +61 -0
- package/landing-page/index.html +37 -0
- package/landing-page/package.json +90 -0
- package/landing-page/postcss.config.mjs +15 -0
- package/landing-page/public/_redirects +1 -0
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +53 -0
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
- package/landing-page/src/app/components/ui/accordion.tsx +66 -0
- package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
- package/landing-page/src/app/components/ui/alert.tsx +66 -0
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
- package/landing-page/src/app/components/ui/avatar.tsx +53 -0
- package/landing-page/src/app/components/ui/badge.tsx +46 -0
- package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
- package/landing-page/src/app/components/ui/button.tsx +57 -0
- package/landing-page/src/app/components/ui/calendar.tsx +75 -0
- package/landing-page/src/app/components/ui/card.tsx +92 -0
- package/landing-page/src/app/components/ui/carousel.tsx +241 -0
- package/landing-page/src/app/components/ui/chart.tsx +353 -0
- package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
- package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
- package/landing-page/src/app/components/ui/command.tsx +177 -0
- package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
- package/landing-page/src/app/components/ui/dialog.tsx +135 -0
- package/landing-page/src/app/components/ui/drawer.tsx +132 -0
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/landing-page/src/app/components/ui/form.tsx +168 -0
- package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
- package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
- package/landing-page/src/app/components/ui/input.tsx +21 -0
- package/landing-page/src/app/components/ui/label.tsx +24 -0
- package/landing-page/src/app/components/ui/menubar.tsx +276 -0
- package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
- package/landing-page/src/app/components/ui/pagination.tsx +127 -0
- package/landing-page/src/app/components/ui/popover.tsx +48 -0
- package/landing-page/src/app/components/ui/progress.tsx +31 -0
- package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
- package/landing-page/src/app/components/ui/resizable.tsx +56 -0
- package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
- package/landing-page/src/app/components/ui/select.tsx +189 -0
- package/landing-page/src/app/components/ui/separator.tsx +28 -0
- package/landing-page/src/app/components/ui/sheet.tsx +139 -0
- package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
- package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
- package/landing-page/src/app/components/ui/slider.tsx +63 -0
- package/landing-page/src/app/components/ui/sonner.tsx +25 -0
- package/landing-page/src/app/components/ui/switch.tsx +31 -0
- package/landing-page/src/app/components/ui/table.tsx +116 -0
- package/landing-page/src/app/components/ui/tabs.tsx +66 -0
- package/landing-page/src/app/components/ui/textarea.tsx +18 -0
- package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
- package/landing-page/src/app/components/ui/toggle.tsx +47 -0
- package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
- package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
- package/landing-page/src/app/components/ui/utils.ts +6 -0
- package/landing-page/src/components/docs/DocsContent.tsx +718 -0
- package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
- package/landing-page/src/components/landing/CTA.tsx +59 -0
- package/landing-page/src/components/landing/Comparison.tsx +84 -0
- package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
- package/landing-page/src/components/landing/Features.tsx +201 -0
- package/landing-page/src/components/landing/Hero.tsx +142 -0
- package/landing-page/src/components/landing/Pricing.tsx +140 -0
- package/landing-page/src/components/landing/Roadmap.tsx +86 -0
- package/landing-page/src/components/landing/Security.tsx +81 -0
- package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
- package/landing-page/src/components/landing/UseCases.tsx +55 -0
- package/landing-page/src/components/landing/Workflow.tsx +101 -0
- package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
- package/landing-page/src/components/layout/Footer.tsx +55 -0
- package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
- package/landing-page/src/components/ui/badge.tsx +39 -0
- package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
- package/landing-page/src/components/ui/button.tsx +57 -0
- package/landing-page/src/components/ui/card.tsx +79 -0
- package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
- package/landing-page/src/components/ui/separator.tsx +28 -0
- package/landing-page/src/lib/utils.ts +6 -0
- package/landing-page/src/main.tsx +10 -0
- package/landing-page/src/pages/Dashboard.tsx +133 -0
- package/landing-page/src/pages/DocsPage.tsx +79 -0
- package/landing-page/src/pages/LandingPage.tsx +31 -0
- package/landing-page/src/pages/TerminalView.tsx +106 -0
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +3 -0
- package/landing-page/src/styles/tailwind.css +4 -0
- package/landing-page/src/styles/theme.css +181 -0
- package/landing-page/vite.config.ts +19 -0
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +20 -0
- package/package.json +74 -0
- package/scripts/build.ts +284 -0
- package/scripts/release.ts +140 -0
- package/src/__tests__/test-utils.ts +298 -0
- package/src/commands/__tests__/serve-messages.test.ts +190 -0
- package/src/commands/access.ts +298 -0
- package/src/commands/add.ts +452 -0
- package/src/commands/auth.ts +364 -0
- package/src/commands/connect.ts +287 -0
- package/src/commands/directory.ts +16 -0
- package/src/commands/host.ts +396 -0
- package/src/commands/identity.ts +184 -0
- package/src/commands/list.ts +200 -0
- package/src/commands/relay.ts +315 -0
- package/src/commands/remove.ts +241 -0
- package/src/commands/serve.ts +1493 -0
- package/src/commands/share.ts +456 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/switch.ts +353 -0
- package/src/commands/tmux.ts +317 -0
- package/src/core/__tests__/access.test.ts +240 -0
- package/src/core/access.ts +277 -0
- package/src/core/bundle.ts +342 -0
- package/src/core/config.ts +510 -0
- package/src/core/git.ts +317 -0
- package/src/core/github.ts +151 -0
- package/src/core/identity.ts +631 -0
- package/src/core/linear.ts +225 -0
- package/src/core/shell.ts +161 -0
- package/src/core/trusted-relays.ts +315 -0
- package/src/index.ts +821 -0
- package/src/lib/remote-session/index.ts +7 -0
- package/src/lib/remote-session/protocol.ts +267 -0
- package/src/lib/remote-session/session-handler.ts +581 -0
- package/src/lib/remote-session/workspace-scanner.ts +167 -0
- package/src/lib/tmux-lite/README.md +81 -0
- package/src/lib/tmux-lite/cli.ts +796 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
- package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
- package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
- package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
- package/src/lib/tmux-lite/crypto/frames.ts +141 -0
- package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
- package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
- package/src/lib/tmux-lite/crypto/identity.ts +286 -0
- package/src/lib/tmux-lite/crypto/index.ts +51 -0
- package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
- package/src/lib/tmux-lite/crypto/invites.ts +215 -0
- package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
- package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
- package/src/lib/tmux-lite/crypto/keys.ts +47 -0
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
- package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
- package/src/lib/tmux-lite/handshake-handler.ts +451 -0
- package/src/lib/tmux-lite/protocol.test.ts +307 -0
- package/src/lib/tmux-lite/protocol.ts +266 -0
- package/src/lib/tmux-lite/relay-client.ts +506 -0
- package/src/lib/tmux-lite/server.ts +1250 -0
- package/src/lib/tmux-lite/shell-integration.sh +37 -0
- package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
- package/src/lib/tmux-lite/terminal-queries.ts +49 -0
- package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
- package/src/relay/__tests__/helpers/auth.ts +354 -0
- package/src/relay/__tests__/helpers/ports.ts +51 -0
- package/src/relay/__tests__/protocol-validation.test.ts +265 -0
- package/src/relay/authorization.ts +303 -0
- package/src/relay/embedded-assets.generated.d.ts +15 -0
- package/src/relay/identity.ts +352 -0
- package/src/relay/index.ts +57 -0
- package/src/relay/pipes.test.ts +427 -0
- package/src/relay/pipes.ts +195 -0
- package/src/relay/protocol.ts +804 -0
- package/src/relay/registries.test.ts +437 -0
- package/src/relay/registries.ts +593 -0
- package/src/relay/server.test.ts +1323 -0
- package/src/relay/server.ts +1092 -0
- package/src/relay/signing.ts +238 -0
- package/src/relay/types.ts +69 -0
- package/src/serve/client-session-manager.ts +622 -0
- package/src/serve/daemon.ts +497 -0
- package/src/serve/pty-session.ts +236 -0
- package/src/serve/types.ts +169 -0
- package/src/shared/components/Flow.tsx +453 -0
- package/src/shared/components/Flow.tui.tsx +343 -0
- package/src/shared/components/Flow.web.tsx +442 -0
- package/src/shared/components/Inbox.tsx +446 -0
- package/src/shared/components/Inbox.tui.tsx +262 -0
- package/src/shared/components/Inbox.web.tsx +329 -0
- package/src/shared/components/MachineList.tsx +187 -0
- package/src/shared/components/MachineList.tui.tsx +161 -0
- package/src/shared/components/MachineList.web.tsx +210 -0
- package/src/shared/components/ProjectList.tsx +176 -0
- package/src/shared/components/ProjectList.tui.tsx +109 -0
- package/src/shared/components/ProjectList.web.tsx +143 -0
- package/src/shared/components/SpacesBrowser.tsx +332 -0
- package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
- package/src/shared/components/SpacesBrowser.web.tsx +221 -0
- package/src/shared/components/index.ts +103 -0
- package/src/shared/hooks/index.ts +16 -0
- package/src/shared/hooks/useNavigation.ts +226 -0
- package/src/shared/index.ts +122 -0
- package/src/shared/providers/LocalMachineProvider.ts +425 -0
- package/src/shared/providers/MachineProvider.ts +165 -0
- package/src/shared/providers/RemoteMachineProvider.ts +444 -0
- package/src/shared/providers/index.ts +26 -0
- package/src/shared/types.ts +145 -0
- package/src/tui/adapters.ts +120 -0
- package/src/tui/app.tsx +1816 -0
- package/src/tui/components/Terminal.tsx +580 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useAppState.ts +314 -0
- package/src/tui/hooks/useDaemonStatus.ts +174 -0
- package/src/tui/hooks/useInboxTUI.ts +113 -0
- package/src/tui/hooks/useRemoteMachines.ts +209 -0
- package/src/tui/index.ts +24 -0
- package/src/tui/state.ts +299 -0
- package/src/tui/terminal-bracketed-paste.test.ts +45 -0
- package/src/tui/terminal-bracketed-paste.ts +47 -0
- package/src/types/bundle.ts +112 -0
- package/src/types/config.ts +89 -0
- package/src/types/errors.ts +206 -0
- package/src/types/identity.ts +284 -0
- package/src/types/workspace-fuzzy.ts +49 -0
- package/src/types/workspace.ts +151 -0
- package/src/utils/bun-socket-writer.ts +80 -0
- package/src/utils/deps.ts +127 -0
- package/src/utils/fuzzy-match.ts +125 -0
- package/src/utils/logger.ts +127 -0
- package/src/utils/markdown.ts +254 -0
- package/src/utils/onboarding.ts +229 -0
- package/src/utils/prompts.ts +114 -0
- package/src/utils/run-commands.ts +112 -0
- package/src/utils/run-scripts.ts +142 -0
- package/src/utils/sanitize.ts +98 -0
- package/src/utils/secrets.ts +122 -0
- package/src/utils/shell-escape.ts +40 -0
- package/src/utils/utf8.ts +79 -0
- package/src/utils/workspace-state.ts +47 -0
- package/src/web/README.md +73 -0
- package/src/web/bun.lock +575 -0
- package/src/web/eslint.config.js +23 -0
- package/src/web/index.html +16 -0
- package/src/web/package.json +37 -0
- package/src/web/public/vite.svg +1 -0
- package/src/web/src/App.tsx +604 -0
- package/src/web/src/assets/react.svg +1 -0
- package/src/web/src/components/Terminal.tsx +207 -0
- package/src/web/src/hooks/useRelayConnection.ts +224 -0
- package/src/web/src/hooks/useTerminal.ts +699 -0
- package/src/web/src/index.css +55 -0
- package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
- package/src/web/src/lib/crypto/frames.ts +205 -0
- package/src/web/src/lib/crypto/handshake.ts +396 -0
- package/src/web/src/lib/crypto/identity.ts +128 -0
- package/src/web/src/lib/crypto/keyexchange.ts +246 -0
- package/src/web/src/lib/crypto/relay-signing.ts +53 -0
- package/src/web/src/lib/invite.ts +58 -0
- package/src/web/src/lib/storage/identity-store.ts +94 -0
- package/src/web/src/main.tsx +10 -0
- package/src/web/src/types/identity.ts +45 -0
- package/src/web/tsconfig.app.json +28 -0
- package/src/web/tsconfig.json +7 -0
- package/src/web/tsconfig.node.json +26 -0
- package/src/web/vite.config.ts +31 -0
- package/todo-security.md +92 -0
- package/tsconfig.json +23 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
- package/worker/bun.lock +237 -0
- package/worker/package.json +22 -0
- package/worker/schema.sql +96 -0
- package/worker/src/handlers/auth.ts +451 -0
- package/worker/src/handlers/subdomains.ts +376 -0
- package/worker/src/handlers/user.ts +98 -0
- package/worker/src/index.ts +70 -0
- package/worker/src/middleware/auth.ts +152 -0
- package/worker/src/services/cloudflare.ts +609 -0
- package/worker/src/types.ts +96 -0
- package/worker/tsconfig.json +15 -0
- package/worker/wrangler.toml +26 -0
package/src/core/git.ts
ADDED
|
@@ -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
|
+
}
|