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/index.ts
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitSpace CLI (gssh) - Main entry point
|
|
5
|
+
* Manages GitHub workspaces with git worktrees and secure remote terminal access
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander'
|
|
9
|
+
import { readFileSync } from 'fs'
|
|
10
|
+
import { join } from 'path'
|
|
11
|
+
import { isFirstTimeSetup, initializeSpaces } from './core/config.js'
|
|
12
|
+
import { logger } from './utils/logger.js'
|
|
13
|
+
import { SpacesError } from './types/errors.js'
|
|
14
|
+
import { addProject, addWorkspace } from './commands/add.js'
|
|
15
|
+
import { switchProject, switchWorkspace } from './commands/switch.js'
|
|
16
|
+
import { listProjects, listWorkspaces } from './commands/list.js'
|
|
17
|
+
import { removeWorkspace, removeProject } from './commands/remove.js'
|
|
18
|
+
import { ensureDependencies } from './utils/deps.js'
|
|
19
|
+
import { getProjectDirectory } from './commands/directory.js'
|
|
20
|
+
import { launchTUI } from './tui/index.js'
|
|
21
|
+
import { addAccessKey, listAccessKeys, removeAccessKey } from './commands/access.js'
|
|
22
|
+
import { createShare } from './commands/share.js'
|
|
23
|
+
import { initIdentity, showIdentity } from './commands/identity.js'
|
|
24
|
+
import { connectToRemote } from './commands/connect.js'
|
|
25
|
+
import { serve, serveStart, serveStop, serveStatus } from './commands/serve.js'
|
|
26
|
+
import { startRelay, authorizeMachine, revokeMachine, listMachines, listTrustedRelays, untrustRelay } from './commands/relay.js'
|
|
27
|
+
import { authLogin, authLogout, authStatus } from './commands/auth.js'
|
|
28
|
+
import { hostReserve, hostRelease, hostList, hostSetPrimary, hostStatus } from './commands/host.js'
|
|
29
|
+
import { startTmux, stopTmux, statusTmux, listTmux, newTmux, attachTmux, killTmux } from './commands/tmux.js'
|
|
30
|
+
import { showStatus } from './commands/status.js'
|
|
31
|
+
|
|
32
|
+
const program = new Command()
|
|
33
|
+
|
|
34
|
+
// Read version from package.json
|
|
35
|
+
let version = '0.0.0'
|
|
36
|
+
try {
|
|
37
|
+
const pkgPath = join(import.meta.dir, '../package.json')
|
|
38
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
39
|
+
version = pkg.version
|
|
40
|
+
} catch {
|
|
41
|
+
// Fallback for compiled binary
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Package info
|
|
45
|
+
program
|
|
46
|
+
.name('gssh')
|
|
47
|
+
.description('GitSpace CLI - Manage GitHub workspaces with secure remote terminal access')
|
|
48
|
+
.version(version)
|
|
49
|
+
|
|
50
|
+
// First-time setup check
|
|
51
|
+
async function checkFirstTimeSetup(): Promise<void> {
|
|
52
|
+
if (isFirstTimeSetup()) {
|
|
53
|
+
logger.bold('Welcome to GitSpace CLI!\n')
|
|
54
|
+
logger.log('Initializing gitspace directory...\n')
|
|
55
|
+
|
|
56
|
+
// Check dependencies
|
|
57
|
+
try {
|
|
58
|
+
await ensureDependencies()
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof SpacesError) {
|
|
61
|
+
logger.error(error.message)
|
|
62
|
+
process.exit(error.exitCode)
|
|
63
|
+
}
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Initialize spaces
|
|
68
|
+
initializeSpaces()
|
|
69
|
+
|
|
70
|
+
logger.success('GitSpace initialized!\n')
|
|
71
|
+
logger.log('Get started by adding a project:')
|
|
72
|
+
logger.command(' gssh add project\n')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Add Commands
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
const addCommand = program
|
|
81
|
+
.command('add')
|
|
82
|
+
.description('Add a new project or workspace')
|
|
83
|
+
|
|
84
|
+
addCommand
|
|
85
|
+
.command('project')
|
|
86
|
+
.description('Add a new project from GitHub')
|
|
87
|
+
.option('--no-clone', 'Create project structure without cloning')
|
|
88
|
+
.option('--org <org>', 'Filter repos to specific organization')
|
|
89
|
+
.option('--linear-key <key>', 'Provide Linear API key via flag')
|
|
90
|
+
.option('--bundle-url <url>', 'Load bundle from remote URL (zip archive)')
|
|
91
|
+
.option('--bundle-path <path>', 'Load bundle from local directory')
|
|
92
|
+
.option('--skip-bundle', 'Skip bundle detection and onboarding')
|
|
93
|
+
.action(async (options) => {
|
|
94
|
+
await checkFirstTimeSetup()
|
|
95
|
+
try {
|
|
96
|
+
await addProject(options)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
handleError(error)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
addCommand
|
|
103
|
+
.argument('[workspace-name]', 'Name of the workspace to create')
|
|
104
|
+
.option(
|
|
105
|
+
'--branch <name>',
|
|
106
|
+
'Specify different branch name from workspace name'
|
|
107
|
+
)
|
|
108
|
+
.option('--from <branch>', 'Create from specific branch instead of base')
|
|
109
|
+
.option('--no-shell', "Don't open interactive shell after creating workspace")
|
|
110
|
+
.option('--no-setup', 'Skip setup commands')
|
|
111
|
+
.action(async (workspaceName, options) => {
|
|
112
|
+
await checkFirstTimeSetup()
|
|
113
|
+
try {
|
|
114
|
+
await addWorkspace(workspaceName, options)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
handleError(error)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Switch Commands
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
const switchCommand = program
|
|
125
|
+
.command('switch')
|
|
126
|
+
.alias('sw')
|
|
127
|
+
.description('Switch to a different project or workspace')
|
|
128
|
+
|
|
129
|
+
switchCommand
|
|
130
|
+
.command('project')
|
|
131
|
+
.description('Switch to a different project')
|
|
132
|
+
.argument('[project-name]', 'Name of the project to switch to')
|
|
133
|
+
.action(async (projectName) => {
|
|
134
|
+
await checkFirstTimeSetup()
|
|
135
|
+
try {
|
|
136
|
+
await switchProject(projectName)
|
|
137
|
+
} catch (error) {
|
|
138
|
+
handleError(error)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
switchCommand
|
|
143
|
+
.argument('[workspace-name]', 'Name of the workspace to switch to')
|
|
144
|
+
.option('--no-shell', "Don't open interactive shell, just print path")
|
|
145
|
+
.option('-f, --force', 'Jump to first fuzzy match without confirmation')
|
|
146
|
+
.action(async (workspaceName, options) => {
|
|
147
|
+
await checkFirstTimeSetup()
|
|
148
|
+
try {
|
|
149
|
+
await switchWorkspace(workspaceName, options)
|
|
150
|
+
} catch (error) {
|
|
151
|
+
handleError(error)
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// List Commands
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
const listCommand = program
|
|
160
|
+
.command('list')
|
|
161
|
+
.alias('ls')
|
|
162
|
+
.description('List projects or workspaces')
|
|
163
|
+
|
|
164
|
+
listCommand
|
|
165
|
+
.command('projects')
|
|
166
|
+
.description('List all projects')
|
|
167
|
+
.option('--json', 'Output in JSON format')
|
|
168
|
+
.option('--verbose', 'Show additional details')
|
|
169
|
+
.action(async (options) => {
|
|
170
|
+
await checkFirstTimeSetup()
|
|
171
|
+
try {
|
|
172
|
+
await listProjects(options)
|
|
173
|
+
} catch (error) {
|
|
174
|
+
handleError(error)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
listCommand
|
|
179
|
+
.command('workspaces')
|
|
180
|
+
.description('List workspaces in current project')
|
|
181
|
+
.option('--json', 'Output in JSON format')
|
|
182
|
+
.option('--verbose', 'Show additional details')
|
|
183
|
+
.action(async (options) => {
|
|
184
|
+
await checkFirstTimeSetup()
|
|
185
|
+
try {
|
|
186
|
+
await listWorkspaces(options)
|
|
187
|
+
} catch (error) {
|
|
188
|
+
handleError(error)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// Default list command (alias for list workspaces)
|
|
193
|
+
listCommand.action(async (options) => {
|
|
194
|
+
await checkFirstTimeSetup()
|
|
195
|
+
try {
|
|
196
|
+
await listWorkspaces(options)
|
|
197
|
+
} catch (error) {
|
|
198
|
+
handleError(error)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Remove Commands
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
const removeCommand = program
|
|
207
|
+
.command('remove')
|
|
208
|
+
.alias('rm')
|
|
209
|
+
.description('Remove a workspace or project')
|
|
210
|
+
|
|
211
|
+
removeCommand
|
|
212
|
+
.command('workspace')
|
|
213
|
+
.description('Remove a workspace')
|
|
214
|
+
.argument('[workspace-name]', 'Name of the workspace to remove')
|
|
215
|
+
.option('--force', 'Skip confirmation prompts')
|
|
216
|
+
.option('--keep-branch', "Don't delete git branch when removing workspace")
|
|
217
|
+
.action(async (workspaceName, options) => {
|
|
218
|
+
await checkFirstTimeSetup()
|
|
219
|
+
try {
|
|
220
|
+
await removeWorkspace(workspaceName, options)
|
|
221
|
+
} catch (error) {
|
|
222
|
+
handleError(error)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
removeCommand
|
|
227
|
+
.command('project')
|
|
228
|
+
.description('Remove a project')
|
|
229
|
+
.argument('[project-name]', 'Name of the project to remove')
|
|
230
|
+
.option('--force', 'Skip confirmation prompts')
|
|
231
|
+
.action(async (projectName, options) => {
|
|
232
|
+
await checkFirstTimeSetup()
|
|
233
|
+
try {
|
|
234
|
+
await removeProject(projectName, options)
|
|
235
|
+
} catch (error) {
|
|
236
|
+
handleError(error)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Default remove command (alias for remove workspace)
|
|
241
|
+
removeCommand.action(async (options) => {
|
|
242
|
+
await checkFirstTimeSetup()
|
|
243
|
+
try {
|
|
244
|
+
await removeWorkspace(undefined, options)
|
|
245
|
+
} catch (error) {
|
|
246
|
+
handleError(error)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// Directory Commands
|
|
252
|
+
// ============================================================================
|
|
253
|
+
|
|
254
|
+
const directoryCommand = program
|
|
255
|
+
.command('directory')
|
|
256
|
+
.alias('dir')
|
|
257
|
+
.description('Manage directories')
|
|
258
|
+
|
|
259
|
+
directoryCommand.action(async (options) => {
|
|
260
|
+
await checkFirstTimeSetup()
|
|
261
|
+
try {
|
|
262
|
+
await getProjectDirectory(options)
|
|
263
|
+
} catch (error) {
|
|
264
|
+
handleError(error)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Identity Commands
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
const identityCommand = program
|
|
273
|
+
.command('identity')
|
|
274
|
+
.description('Manage machine identity for secure remote connections')
|
|
275
|
+
|
|
276
|
+
identityCommand
|
|
277
|
+
.command('init')
|
|
278
|
+
.description('Initialize a new identity keypair')
|
|
279
|
+
.option('--force', 'Overwrite existing identity')
|
|
280
|
+
.action(async (options) => {
|
|
281
|
+
await checkFirstTimeSetup()
|
|
282
|
+
try {
|
|
283
|
+
await initIdentity(options)
|
|
284
|
+
} catch (error) {
|
|
285
|
+
handleError(error)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
identityCommand
|
|
290
|
+
.command('show')
|
|
291
|
+
.description('Show identity information')
|
|
292
|
+
.option('--fingerprint', 'Show only fingerprint')
|
|
293
|
+
.option('--json', 'Output in JSON format')
|
|
294
|
+
.action(async (options) => {
|
|
295
|
+
await checkFirstTimeSetup()
|
|
296
|
+
try {
|
|
297
|
+
await showIdentity(options)
|
|
298
|
+
} catch (error) {
|
|
299
|
+
handleError(error)
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// ============================================================================
|
|
304
|
+
// Access Commands
|
|
305
|
+
// ============================================================================
|
|
306
|
+
|
|
307
|
+
const accessCommand = program
|
|
308
|
+
.command('access')
|
|
309
|
+
.description('Manage access control for remote connections')
|
|
310
|
+
|
|
311
|
+
accessCommand
|
|
312
|
+
.command('add')
|
|
313
|
+
.description('Add a new access key (grants full access)')
|
|
314
|
+
.argument('<pubkey>', 'Public key (gssh-pub:SIGNING:KEYEXCHANGE or just SIGNING)')
|
|
315
|
+
.option('--label <name>', 'Human-readable label for this key')
|
|
316
|
+
.action(async (pubkey, options) => {
|
|
317
|
+
await checkFirstTimeSetup()
|
|
318
|
+
try {
|
|
319
|
+
await addAccessKey(pubkey, options)
|
|
320
|
+
} catch (error) {
|
|
321
|
+
handleError(error)
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
accessCommand
|
|
326
|
+
.command('list')
|
|
327
|
+
.alias('ls')
|
|
328
|
+
.description('List all access keys')
|
|
329
|
+
.option('--json', 'Output in JSON format')
|
|
330
|
+
.action(async (options) => {
|
|
331
|
+
await checkFirstTimeSetup()
|
|
332
|
+
try {
|
|
333
|
+
await listAccessKeys(options)
|
|
334
|
+
} catch (error) {
|
|
335
|
+
handleError(error)
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
accessCommand
|
|
340
|
+
.command('remove')
|
|
341
|
+
.alias('rm')
|
|
342
|
+
.description('Remove an access key')
|
|
343
|
+
.argument('<pubkey|label>', 'Public key, identity ID prefix, or label')
|
|
344
|
+
.option('--force', 'Skip confirmation prompt')
|
|
345
|
+
.action(async (pubkeyOrLabel, options) => {
|
|
346
|
+
await checkFirstTimeSetup()
|
|
347
|
+
try {
|
|
348
|
+
await removeAccessKey(pubkeyOrLabel, options)
|
|
349
|
+
} catch (error) {
|
|
350
|
+
handleError(error)
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Share Commands
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
const shareCommand = program
|
|
359
|
+
.command('share')
|
|
360
|
+
.description('Share workspace access via invite tokens')
|
|
361
|
+
|
|
362
|
+
shareCommand
|
|
363
|
+
.command('create')
|
|
364
|
+
.description('Create a share invite token (view-only session access)')
|
|
365
|
+
.option('--expires <duration>', 'Token validity duration (e.g., 1h, 24h, 7d, 1w)', '24h')
|
|
366
|
+
.option('--session <id>', 'Specific session ID to share (defaults to current session)')
|
|
367
|
+
.option('--relay <url>', 'Relay server URL', 'wss://relay.gitspace.sh')
|
|
368
|
+
.action(async (options) => {
|
|
369
|
+
await checkFirstTimeSetup()
|
|
370
|
+
try {
|
|
371
|
+
await createShare(options)
|
|
372
|
+
} catch (error) {
|
|
373
|
+
handleError(error)
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// Connect Command
|
|
379
|
+
// ============================================================================
|
|
380
|
+
|
|
381
|
+
program
|
|
382
|
+
.command('connect')
|
|
383
|
+
.description('Connect to a remote machine via invite token')
|
|
384
|
+
.argument('[invite]', 'Invite token or URL (https://gitspace.sh/join#...)')
|
|
385
|
+
.option('--relay <url>', 'Override relay URL from invite token')
|
|
386
|
+
.action(async (invite, options) => {
|
|
387
|
+
await checkFirstTimeSetup()
|
|
388
|
+
try {
|
|
389
|
+
await connectToRemote(invite, options)
|
|
390
|
+
} catch (error) {
|
|
391
|
+
handleError(error)
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// ============================================================================
|
|
396
|
+
// Serve Command
|
|
397
|
+
// ============================================================================
|
|
398
|
+
|
|
399
|
+
const serveCommand = program
|
|
400
|
+
.command('serve')
|
|
401
|
+
.description('Manage remote access daemon')
|
|
402
|
+
|
|
403
|
+
serveCommand
|
|
404
|
+
.command('start')
|
|
405
|
+
.description('Start the serve daemon')
|
|
406
|
+
.option('--relay <url>', 'Override default relay URL')
|
|
407
|
+
.option('--relay-pubkey <pubkey>', 'Relay public key for explicit trust (base64)')
|
|
408
|
+
.option('--password-stdin', 'Read password from stdin')
|
|
409
|
+
.option('--foreground', 'Run in foreground (don\'t daemonize)')
|
|
410
|
+
.action(async (options) => {
|
|
411
|
+
await checkFirstTimeSetup()
|
|
412
|
+
try {
|
|
413
|
+
await serveStart(options)
|
|
414
|
+
} catch (error) {
|
|
415
|
+
handleError(error)
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
serveCommand
|
|
420
|
+
.command('stop')
|
|
421
|
+
.description('Stop the serve daemon')
|
|
422
|
+
.action(async () => {
|
|
423
|
+
try {
|
|
424
|
+
await serveStop()
|
|
425
|
+
} catch (error) {
|
|
426
|
+
handleError(error)
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
serveCommand
|
|
431
|
+
.command('status')
|
|
432
|
+
.description('Show serve daemon status')
|
|
433
|
+
.action(async () => {
|
|
434
|
+
try {
|
|
435
|
+
await serveStatus()
|
|
436
|
+
} catch (error) {
|
|
437
|
+
handleError(error)
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
// Default action for 'gssh serve' (backwards compatibility - same as start)
|
|
442
|
+
serveCommand
|
|
443
|
+
.option('--relay <url>', 'Override default relay URL')
|
|
444
|
+
.option('--relay-pubkey <pubkey>', 'Relay public key for explicit trust (base64)')
|
|
445
|
+
.action(async (options) => {
|
|
446
|
+
await checkFirstTimeSetup()
|
|
447
|
+
try {
|
|
448
|
+
// Default to interactive (non-daemon) mode for backwards compatibility
|
|
449
|
+
await serve(options)
|
|
450
|
+
} catch (error) {
|
|
451
|
+
handleError(error)
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// ============================================================================
|
|
456
|
+
// Relay Commands
|
|
457
|
+
// ============================================================================
|
|
458
|
+
|
|
459
|
+
const relayCommand = program
|
|
460
|
+
.command('relay')
|
|
461
|
+
.description('Manage relay server')
|
|
462
|
+
|
|
463
|
+
relayCommand
|
|
464
|
+
.command('start')
|
|
465
|
+
.description('Start the relay server')
|
|
466
|
+
.option('--port <port>', 'Port to listen on', '4480')
|
|
467
|
+
.option('--bind <address>', 'Address to bind to', '0.0.0.0')
|
|
468
|
+
.option('--hostname <host>', 'Only serve requests for this domain (optional)')
|
|
469
|
+
.option('--label <label>', 'Human-readable label for this relay')
|
|
470
|
+
.action(async (options) => {
|
|
471
|
+
try {
|
|
472
|
+
await startRelay({
|
|
473
|
+
port: parseInt(options.port, 10),
|
|
474
|
+
bind: options.bind,
|
|
475
|
+
hostname: options.hostname,
|
|
476
|
+
label: options.label,
|
|
477
|
+
})
|
|
478
|
+
} catch (error) {
|
|
479
|
+
handleError(error)
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
relayCommand
|
|
484
|
+
.command('authorize')
|
|
485
|
+
.description('Authorize a machine to connect to this relay')
|
|
486
|
+
.argument('<pubkey>', 'Machine public key in gssh-pub:SIGNING:KEYEXCHANGE format')
|
|
487
|
+
.option('--label <label>', 'Human-readable label for this machine')
|
|
488
|
+
.action(async (pubkey, options) => {
|
|
489
|
+
try {
|
|
490
|
+
await authorizeMachine(pubkey, { label: options.label })
|
|
491
|
+
} catch (error) {
|
|
492
|
+
handleError(error)
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
relayCommand
|
|
497
|
+
.command('revoke')
|
|
498
|
+
.description("Revoke a machine's authorization")
|
|
499
|
+
.argument('<fingerprint-or-label>', 'Fingerprint or label of machine to revoke')
|
|
500
|
+
.action(async (fingerprintOrLabel) => {
|
|
501
|
+
try {
|
|
502
|
+
await revokeMachine(fingerprintOrLabel)
|
|
503
|
+
} catch (error) {
|
|
504
|
+
handleError(error)
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
relayCommand
|
|
509
|
+
.command('machines')
|
|
510
|
+
.description('List authorized machines')
|
|
511
|
+
.action(async () => {
|
|
512
|
+
try {
|
|
513
|
+
await listMachines()
|
|
514
|
+
} catch (error) {
|
|
515
|
+
handleError(error)
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
relayCommand
|
|
520
|
+
.command('trusted')
|
|
521
|
+
.description('List trusted relays (machine-side)')
|
|
522
|
+
.action(async () => {
|
|
523
|
+
try {
|
|
524
|
+
await listTrustedRelays()
|
|
525
|
+
} catch (error) {
|
|
526
|
+
handleError(error)
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
relayCommand
|
|
531
|
+
.command('untrust')
|
|
532
|
+
.description('Remove trust for a relay (machine-side)')
|
|
533
|
+
.argument('<url-or-fingerprint>', 'URL, fingerprint, or label of relay to untrust')
|
|
534
|
+
.action(async (urlOrFingerprint) => {
|
|
535
|
+
try {
|
|
536
|
+
await untrustRelay(urlOrFingerprint)
|
|
537
|
+
} catch (error) {
|
|
538
|
+
handleError(error)
|
|
539
|
+
}
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
// ============================================================================
|
|
544
|
+
// Tmux Commands (tmux-lite daemon management)
|
|
545
|
+
// ============================================================================
|
|
546
|
+
|
|
547
|
+
const tmuxCommand = program
|
|
548
|
+
.command('tmux')
|
|
549
|
+
.description('Manage tmux-lite terminal session daemon')
|
|
550
|
+
|
|
551
|
+
tmuxCommand
|
|
552
|
+
.command('start')
|
|
553
|
+
.description('Start the tmux-lite server daemon')
|
|
554
|
+
.action(async () => {
|
|
555
|
+
try {
|
|
556
|
+
await startTmux()
|
|
557
|
+
} catch (error) {
|
|
558
|
+
handleError(error)
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
tmuxCommand
|
|
563
|
+
.command('stop')
|
|
564
|
+
.description('Stop the tmux-lite server daemon')
|
|
565
|
+
.option('--force', 'Stop even if sessions are active')
|
|
566
|
+
.action(async (options) => {
|
|
567
|
+
try {
|
|
568
|
+
await stopTmux({ force: options.force })
|
|
569
|
+
} catch (error) {
|
|
570
|
+
handleError(error)
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
tmuxCommand
|
|
575
|
+
.command('status')
|
|
576
|
+
.description('Show tmux-lite server status')
|
|
577
|
+
.action(async () => {
|
|
578
|
+
try {
|
|
579
|
+
await statusTmux()
|
|
580
|
+
} catch (error) {
|
|
581
|
+
handleError(error)
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
tmuxCommand
|
|
586
|
+
.command('list')
|
|
587
|
+
.description('List active tmux-lite sessions')
|
|
588
|
+
.action(async () => {
|
|
589
|
+
try {
|
|
590
|
+
await listTmux()
|
|
591
|
+
} catch (error) {
|
|
592
|
+
handleError(error)
|
|
593
|
+
}
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
tmuxCommand
|
|
597
|
+
.command('new [name]')
|
|
598
|
+
.description('Create and attach to a new session')
|
|
599
|
+
.action(async (name) => {
|
|
600
|
+
try {
|
|
601
|
+
await newTmux(name)
|
|
602
|
+
} catch (error) {
|
|
603
|
+
handleError(error)
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
tmuxCommand
|
|
608
|
+
.command('attach <id>')
|
|
609
|
+
.description('Attach to a session (by id or name)')
|
|
610
|
+
.option('--force', 'Take over if attached elsewhere')
|
|
611
|
+
.action(async (id, options) => {
|
|
612
|
+
try {
|
|
613
|
+
await attachTmux(id, { force: options.force })
|
|
614
|
+
} catch (error) {
|
|
615
|
+
handleError(error)
|
|
616
|
+
}
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
tmuxCommand
|
|
620
|
+
.command('kill <id>')
|
|
621
|
+
.description('Kill a session (by id or name)')
|
|
622
|
+
.action(async (id) => {
|
|
623
|
+
try {
|
|
624
|
+
await killTmux(id)
|
|
625
|
+
} catch (error) {
|
|
626
|
+
handleError(error)
|
|
627
|
+
}
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
// ============================================================================
|
|
631
|
+
// Auth Commands (gitspace.sh)
|
|
632
|
+
// ============================================================================
|
|
633
|
+
|
|
634
|
+
const authCommand = program
|
|
635
|
+
.command('auth')
|
|
636
|
+
.description('Manage gitspace.sh authentication')
|
|
637
|
+
|
|
638
|
+
authCommand
|
|
639
|
+
.command('login')
|
|
640
|
+
.description('Login with GitHub')
|
|
641
|
+
.action(async () => {
|
|
642
|
+
await checkFirstTimeSetup()
|
|
643
|
+
try {
|
|
644
|
+
await authLogin()
|
|
645
|
+
} catch (error) {
|
|
646
|
+
handleError(error)
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
authCommand
|
|
651
|
+
.command('logout')
|
|
652
|
+
.description('Logout and clear credentials')
|
|
653
|
+
.action(async () => {
|
|
654
|
+
try {
|
|
655
|
+
await authLogout()
|
|
656
|
+
} catch (error) {
|
|
657
|
+
handleError(error)
|
|
658
|
+
}
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
authCommand
|
|
662
|
+
.command('status')
|
|
663
|
+
.description('Show login status')
|
|
664
|
+
.action(async () => {
|
|
665
|
+
try {
|
|
666
|
+
await authStatus()
|
|
667
|
+
} catch (error) {
|
|
668
|
+
handleError(error)
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
// ============================================================================
|
|
673
|
+
// Host Commands (gitspace.sh hosting)
|
|
674
|
+
// ============================================================================
|
|
675
|
+
|
|
676
|
+
const hostCommand = program
|
|
677
|
+
.command('host')
|
|
678
|
+
.description('Manage gitspace.sh hosting')
|
|
679
|
+
|
|
680
|
+
hostCommand
|
|
681
|
+
.command('reserve <subdomain>')
|
|
682
|
+
.description('Reserve a subdomain (e.g., brad.gitspace.sh)')
|
|
683
|
+
.action(async (subdomain) => {
|
|
684
|
+
await checkFirstTimeSetup()
|
|
685
|
+
try {
|
|
686
|
+
await hostReserve(subdomain)
|
|
687
|
+
} catch (error) {
|
|
688
|
+
handleError(error)
|
|
689
|
+
}
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
hostCommand
|
|
693
|
+
.command('release [subdomain]')
|
|
694
|
+
.description('Release a subdomain')
|
|
695
|
+
.action(async (subdomain) => {
|
|
696
|
+
try {
|
|
697
|
+
await hostRelease(subdomain)
|
|
698
|
+
} catch (error) {
|
|
699
|
+
handleError(error)
|
|
700
|
+
}
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
hostCommand
|
|
704
|
+
.command('list')
|
|
705
|
+
.alias('ls')
|
|
706
|
+
.description('List your subdomains')
|
|
707
|
+
.action(async () => {
|
|
708
|
+
try {
|
|
709
|
+
await hostList()
|
|
710
|
+
} catch (error) {
|
|
711
|
+
handleError(error)
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
hostCommand
|
|
716
|
+
.command('set-primary <subdomain>')
|
|
717
|
+
.description('Set primary subdomain for `gssh serve`')
|
|
718
|
+
.action(async (subdomain) => {
|
|
719
|
+
try {
|
|
720
|
+
await hostSetPrimary(subdomain)
|
|
721
|
+
} catch (error) {
|
|
722
|
+
handleError(error)
|
|
723
|
+
}
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
hostCommand
|
|
727
|
+
.command('status')
|
|
728
|
+
.description('Show hosting status')
|
|
729
|
+
.action(async () => {
|
|
730
|
+
try {
|
|
731
|
+
await hostStatus()
|
|
732
|
+
} catch (error) {
|
|
733
|
+
handleError(error)
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
// ============================================================================
|
|
738
|
+
// Status Command (unified daemon status)
|
|
739
|
+
// ============================================================================
|
|
740
|
+
|
|
741
|
+
program
|
|
742
|
+
.command('status')
|
|
743
|
+
.description('Show status of all spaces daemons')
|
|
744
|
+
.action(async () => {
|
|
745
|
+
try {
|
|
746
|
+
await showStatus()
|
|
747
|
+
} catch (error) {
|
|
748
|
+
handleError(error)
|
|
749
|
+
}
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
// ============================================================================
|
|
753
|
+
// Error Handling
|
|
754
|
+
// ============================================================================
|
|
755
|
+
|
|
756
|
+
function handleError(error: unknown): never {
|
|
757
|
+
if (error instanceof SpacesError) {
|
|
758
|
+
logger.error(error.message)
|
|
759
|
+
process.exit(error.exitCode)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (error instanceof Error) {
|
|
763
|
+
logger.error(`Unexpected error: ${error.message}`)
|
|
764
|
+
logger.debug(error.stack || '')
|
|
765
|
+
process.exit(1)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
logger.error('An unexpected error occurred')
|
|
769
|
+
process.exit(1)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ============================================================================
|
|
773
|
+
// Parse and Execute
|
|
774
|
+
// ============================================================================
|
|
775
|
+
|
|
776
|
+
// Handle uncaught errors
|
|
777
|
+
process.on('uncaughtException', (error) => {
|
|
778
|
+
logger.error(`Uncaught exception: ${error.message}`)
|
|
779
|
+
logger.debug(error.stack || '')
|
|
780
|
+
process.exit(1)
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
process.on('unhandledRejection', (reason) => {
|
|
784
|
+
logger.error(`Unhandled rejection: ${reason}`)
|
|
785
|
+
process.exit(1)
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
// Parse command line arguments
|
|
789
|
+
// Check for global relay options (TUI mode with relay)
|
|
790
|
+
const args = process.argv.slice(2)
|
|
791
|
+
const hasRelayOption = args.includes('--relay')
|
|
792
|
+
const hasOnlyRelayOptions = args.every(arg =>
|
|
793
|
+
arg === '--relay' ||
|
|
794
|
+
(args[args.indexOf('--relay') + 1] === arg)
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
// If no args provided or only relay options, launch TUI
|
|
798
|
+
if (process.argv.length === 2 || (hasRelayOption && hasOnlyRelayOptions)) {
|
|
799
|
+
// Extract options manually instead of using commander.parse() which shows help
|
|
800
|
+
const relayIndex = args.indexOf('--relay')
|
|
801
|
+
const relayUrl = relayIndex >= 0 ? args[relayIndex + 1] : undefined
|
|
802
|
+
|
|
803
|
+
// Build relay config if provided (auth now via challenge-response, not token)
|
|
804
|
+
const relayConfig = relayUrl ? {
|
|
805
|
+
url: relayUrl,
|
|
806
|
+
} : undefined
|
|
807
|
+
|
|
808
|
+
// Launch TUI
|
|
809
|
+
checkFirstTimeSetup()
|
|
810
|
+
.then(() => launchTUI(relayConfig))
|
|
811
|
+
.catch((error) => {
|
|
812
|
+
if (error instanceof SpacesError) {
|
|
813
|
+
logger.error(error.message)
|
|
814
|
+
process.exit(error.exitCode)
|
|
815
|
+
}
|
|
816
|
+
logger.error(`Failed to launch TUI: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
817
|
+
process.exit(1)
|
|
818
|
+
})
|
|
819
|
+
} else {
|
|
820
|
+
program.parse()
|
|
821
|
+
}
|