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
|
@@ -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
|
+
}
|