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,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List command implementation
|
|
3
|
+
* Handles 'gssh list' (workspaces), 'gssh list projects', and 'gssh list workspaces'
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import {
|
|
9
|
+
getAllProjectNames,
|
|
10
|
+
readProjectConfig,
|
|
11
|
+
getCurrentProject,
|
|
12
|
+
getProjectWorkspacesDir,
|
|
13
|
+
readGlobalConfig,
|
|
14
|
+
} from '../core/config.js'
|
|
15
|
+
import { getWorktreeInfo } from '../core/git.js'
|
|
16
|
+
import { logger } from '../utils/logger.js'
|
|
17
|
+
import { SpacesError, NoProjectError } from '../types/errors.js'
|
|
18
|
+
import type { ProjectInfo, WorktreeInfo } from '../types/workspace.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* List all projects
|
|
22
|
+
*/
|
|
23
|
+
export async function listProjects(
|
|
24
|
+
options: {
|
|
25
|
+
json?: boolean
|
|
26
|
+
verbose?: boolean
|
|
27
|
+
} = {}
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const projectNames = getAllProjectNames()
|
|
30
|
+
|
|
31
|
+
if (projectNames.length === 0) {
|
|
32
|
+
logger.info('No projects found')
|
|
33
|
+
logger.log('\nCreate a project:\n gssh add project')
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const currentProject = getCurrentProject()
|
|
38
|
+
const projects: ProjectInfo[] = []
|
|
39
|
+
|
|
40
|
+
for (const name of projectNames) {
|
|
41
|
+
const config = readProjectConfig(name)
|
|
42
|
+
const workspacesDir = getProjectWorkspacesDir(name)
|
|
43
|
+
|
|
44
|
+
let workspaceCount = 0
|
|
45
|
+
if (existsSync(workspacesDir)) {
|
|
46
|
+
workspaceCount = readdirSync(workspacesDir).length
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
projects.push({
|
|
50
|
+
name,
|
|
51
|
+
repository: config.repository,
|
|
52
|
+
path: workspacesDir,
|
|
53
|
+
workspaceCount,
|
|
54
|
+
isCurrent: name === currentProject,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.json) {
|
|
59
|
+
console.log(JSON.stringify(projects, null, 2))
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.bold('Projects:')
|
|
64
|
+
|
|
65
|
+
for (const project of projects) {
|
|
66
|
+
const indicator = project.isCurrent ? ' *' : ' '
|
|
67
|
+
const currentLabel = project.isCurrent ? ' (current)' : ''
|
|
68
|
+
|
|
69
|
+
if (options.verbose) {
|
|
70
|
+
logger.log(
|
|
71
|
+
`${indicator} ${project.name.padEnd(20)} ${project.repository.padEnd(
|
|
72
|
+
30
|
|
73
|
+
)} ${project.workspaceCount} workspaces${currentLabel}`
|
|
74
|
+
)
|
|
75
|
+
} else {
|
|
76
|
+
logger.log(
|
|
77
|
+
`${indicator} ${project.name.padEnd(20)} ${
|
|
78
|
+
project.repository
|
|
79
|
+
}${currentLabel}`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Calculate days since last commit
|
|
87
|
+
*/
|
|
88
|
+
function daysSinceCommit(date: Date): number {
|
|
89
|
+
const now = new Date()
|
|
90
|
+
const diff = now.getTime() - date.getTime()
|
|
91
|
+
return Math.floor(diff / (1000 * 60 * 60 * 24))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* List workspaces in the current project
|
|
96
|
+
*/
|
|
97
|
+
export async function listWorkspaces(
|
|
98
|
+
options: {
|
|
99
|
+
json?: boolean
|
|
100
|
+
verbose?: boolean
|
|
101
|
+
} = {}
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const currentProject = getCurrentProject()
|
|
104
|
+
if (!currentProject) {
|
|
105
|
+
throw new NoProjectError()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const workspacesDir = getProjectWorkspacesDir(currentProject)
|
|
109
|
+
|
|
110
|
+
if (!existsSync(workspacesDir)) {
|
|
111
|
+
logger.info(`No workspaces found in project "${currentProject}"`)
|
|
112
|
+
logger.log('\nCreate a workspace:\n gssh add')
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const workspaceNames = readdirSync(workspacesDir).filter((entry) => {
|
|
117
|
+
const path = join(workspacesDir, entry)
|
|
118
|
+
return existsSync(path)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (workspaceNames.length === 0) {
|
|
122
|
+
logger.info(`No workspaces found in project "${currentProject}"`)
|
|
123
|
+
logger.log('\nCreate a workspace:\n gssh add')
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Get workspace info
|
|
128
|
+
const workspaces: WorktreeInfo[] = []
|
|
129
|
+
const globalConfig = readGlobalConfig()
|
|
130
|
+
|
|
131
|
+
for (const name of workspaceNames) {
|
|
132
|
+
const workspacePath = join(workspacesDir, name)
|
|
133
|
+
const info = await getWorktreeInfo(workspacePath)
|
|
134
|
+
|
|
135
|
+
if (info) {
|
|
136
|
+
workspaces.push(info)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.json) {
|
|
141
|
+
console.log(JSON.stringify(workspaces, null, 2))
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
logger.bold(`Workspaces (${currentProject}):`)
|
|
146
|
+
|
|
147
|
+
for (const workspace of workspaces) {
|
|
148
|
+
const parts: string[] = []
|
|
149
|
+
parts.push(addSpace(2)) // indent
|
|
150
|
+
parts.push(truncateName(workspace.name, 40).padEnd(45)) // workspace name
|
|
151
|
+
|
|
152
|
+
// Branch and ahead/behind
|
|
153
|
+
if (workspace.ahead > 0 || workspace.behind > 0) {
|
|
154
|
+
parts.push(`+${workspace.ahead} -${workspace.behind}`.padEnd(10))
|
|
155
|
+
} else {
|
|
156
|
+
parts.push('+0 -0'.padEnd(10))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Uncommitted changes
|
|
160
|
+
if (workspace.uncommittedChanges > 0) {
|
|
161
|
+
parts.push(`${workspace.uncommittedChanges} uncommitted`.padEnd(20))
|
|
162
|
+
} else {
|
|
163
|
+
parts.push('clean'.padEnd(20))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Stale workspace warning
|
|
167
|
+
const daysSince = daysSinceCommit(workspace.lastCommitDate)
|
|
168
|
+
if (daysSince > globalConfig.staleDays) {
|
|
169
|
+
parts.push(`[stale: ${daysSince} days]`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
logger.log(parts.join(' '))
|
|
173
|
+
|
|
174
|
+
// Verbose mode: show last commit
|
|
175
|
+
if (options.verbose) {
|
|
176
|
+
logger.dim(` Last commit: ${workspace.lastCommit}`)
|
|
177
|
+
logger.dim(` Date: ${workspace.lastCommitDate.toLocaleDateString()}\n`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function truncateName(
|
|
183
|
+
name: string,
|
|
184
|
+
maxLength: number,
|
|
185
|
+
includeEllipsis = true
|
|
186
|
+
): string {
|
|
187
|
+
if (name.length <= maxLength) {
|
|
188
|
+
return name
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (includeEllipsis) {
|
|
192
|
+
return name.substring(0, maxLength - 3) + '...'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return name.substring(0, maxLength)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function addSpace(size: number): string {
|
|
199
|
+
return ' '.repeat(size)
|
|
200
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay command implementation
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - `gssh relay start` - Start the relay server
|
|
6
|
+
* - `gssh relay authorize` - Authorize a machine
|
|
7
|
+
* - `gssh relay revoke` - Revoke a machine's authorization
|
|
8
|
+
* - `gssh relay machines` - List authorized machines
|
|
9
|
+
* - `gssh relay trusted` - List trusted relays (machine-side)
|
|
10
|
+
* - `gssh relay untrust` - Remove relay trust (machine-side)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logger } from "../utils/logger.js";
|
|
14
|
+
import { createRelayServer } from "../relay/server.js";
|
|
15
|
+
import { SpacesError } from "../types/errors.js";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import {
|
|
18
|
+
loadOrCreateRelayIdentity,
|
|
19
|
+
formatRelayFingerprint,
|
|
20
|
+
type RelayIdentity,
|
|
21
|
+
} from "../relay/identity.js";
|
|
22
|
+
import {
|
|
23
|
+
getAuthorizedMachines,
|
|
24
|
+
addAuthorizedMachine,
|
|
25
|
+
removeAuthorizedMachine,
|
|
26
|
+
computeMachineFingerprint,
|
|
27
|
+
type AuthorizedMachine,
|
|
28
|
+
} from "../relay/authorization.js";
|
|
29
|
+
import {
|
|
30
|
+
getTrustedRelays,
|
|
31
|
+
removeTrustedRelay,
|
|
32
|
+
type TrustedRelay,
|
|
33
|
+
} from "../core/trusted-relays.js";
|
|
34
|
+
|
|
35
|
+
/** Default port for relay server (4480 = "GIT0" on phone keypad) */
|
|
36
|
+
const DEFAULT_PORT = 4480;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Start the relay server
|
|
40
|
+
*
|
|
41
|
+
* @param options - Command options
|
|
42
|
+
*/
|
|
43
|
+
export async function startRelay(options: {
|
|
44
|
+
port?: number;
|
|
45
|
+
hostname?: string;
|
|
46
|
+
bind?: string;
|
|
47
|
+
label?: string;
|
|
48
|
+
}): Promise<void> {
|
|
49
|
+
const port = options.port ?? parseInt(process.env.PORT ?? String(DEFAULT_PORT), 10);
|
|
50
|
+
const bind = options.bind ?? process.env.RELAY_BIND ?? "0.0.0.0";
|
|
51
|
+
const hostname = options.hostname ?? process.env.RELAY_HOST;
|
|
52
|
+
|
|
53
|
+
// Load or create relay identity
|
|
54
|
+
const identity = await loadOrCreateRelayIdentity(options.label);
|
|
55
|
+
const fingerprint = formatRelayFingerprint(identity.signingPublicKey);
|
|
56
|
+
|
|
57
|
+
// Display relay identity prominently
|
|
58
|
+
logger.log("");
|
|
59
|
+
logger.log(chalk.cyan("┌────────────────────────────────────────────────┐"));
|
|
60
|
+
logger.log(chalk.cyan("│") + chalk.bold(" Relay Identity ") + chalk.cyan("│"));
|
|
61
|
+
logger.log(chalk.cyan("│") + ` Fingerprint: ${chalk.yellow(fingerprint)} ` + chalk.cyan("│"));
|
|
62
|
+
if (identity.label) {
|
|
63
|
+
const labelPadded = identity.label.substring(0, 30).padEnd(30);
|
|
64
|
+
logger.log(chalk.cyan("│") + ` Label: ${chalk.dim(labelPadded)} ` + chalk.cyan("│"));
|
|
65
|
+
}
|
|
66
|
+
logger.log(chalk.cyan("│") + ` Public Key: ` + chalk.cyan("│"));
|
|
67
|
+
logger.log(chalk.cyan("│") + ` ${chalk.dim(identity.signingPublicKey.substring(0, 44))} ` + chalk.cyan("│"));
|
|
68
|
+
logger.log(chalk.cyan("└────────────────────────────────────────────────┘"));
|
|
69
|
+
logger.log("");
|
|
70
|
+
|
|
71
|
+
logger.log(` Port: ${port}`);
|
|
72
|
+
logger.log(` Bind: ${bind}`);
|
|
73
|
+
if (hostname) {
|
|
74
|
+
logger.log(` Hostname: ${hostname} (only serving this domain)`);
|
|
75
|
+
}
|
|
76
|
+
logger.log("");
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const server = await createRelayServer({
|
|
80
|
+
port,
|
|
81
|
+
bind,
|
|
82
|
+
hostname,
|
|
83
|
+
identity,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
logger.success(`Relay listening on ws://${hostname || bind}:${port}`);
|
|
87
|
+
logger.log("");
|
|
88
|
+
logger.dim("Press Ctrl+C to stop");
|
|
89
|
+
logger.log("");
|
|
90
|
+
|
|
91
|
+
// Set up shutdown handlers
|
|
92
|
+
const shutdown = () => {
|
|
93
|
+
logger.log("");
|
|
94
|
+
logger.info("Shutting down relay...");
|
|
95
|
+
server.stop();
|
|
96
|
+
process.exit(0);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
process.on("SIGINT", shutdown);
|
|
100
|
+
process.on("SIGTERM", shutdown);
|
|
101
|
+
|
|
102
|
+
// Keep process alive
|
|
103
|
+
await new Promise(() => {
|
|
104
|
+
// Never resolves
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new SpacesError(
|
|
108
|
+
`Failed to start relay: ${error instanceof Error ? error.message : String(error)}`,
|
|
109
|
+
"SYSTEM_ERROR",
|
|
110
|
+
2
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Authorization Commands
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Authorize a machine to connect to this relay
|
|
121
|
+
*
|
|
122
|
+
* @param spacesPubKey - Machine public key in gssh-pub:SIGNING:KEYEXCHANGE format
|
|
123
|
+
* @param options - Command options
|
|
124
|
+
*/
|
|
125
|
+
export async function authorizeMachine(
|
|
126
|
+
spacesPubKey: string,
|
|
127
|
+
options: { label?: string }
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
const entry = addAuthorizedMachine(spacesPubKey, options.label);
|
|
130
|
+
|
|
131
|
+
if (!entry) {
|
|
132
|
+
throw new SpacesError(
|
|
133
|
+
`Invalid public key format. Expected: gssh-pub:SIGNING_KEY:KEYEXCHANGE_KEY\n` +
|
|
134
|
+
`Get this from \`gssh identity show\` on the machine you want to authorize.`,
|
|
135
|
+
"USER_ERROR",
|
|
136
|
+
1
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logger.log("");
|
|
141
|
+
logger.success("Machine authorized!");
|
|
142
|
+
logger.log("");
|
|
143
|
+
logger.log(` Fingerprint: ${chalk.cyan(entry.fingerprint)}`);
|
|
144
|
+
if (entry.label) {
|
|
145
|
+
logger.log(` Label: ${chalk.yellow(entry.label)}`);
|
|
146
|
+
}
|
|
147
|
+
logger.log("");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Revoke a machine's authorization
|
|
152
|
+
*
|
|
153
|
+
* @param fingerprintOrLabel - Fingerprint or label of machine to revoke
|
|
154
|
+
*/
|
|
155
|
+
export async function revokeMachine(fingerprintOrLabel: string): Promise<void> {
|
|
156
|
+
const removed = removeAuthorizedMachine(fingerprintOrLabel);
|
|
157
|
+
|
|
158
|
+
if (!removed) {
|
|
159
|
+
const machines = getAuthorizedMachines();
|
|
160
|
+
|
|
161
|
+
if (machines.length === 0) {
|
|
162
|
+
throw new SpacesError("No machines are authorized.", "USER_ERROR", 1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
logger.error(`No machine found matching: ${fingerprintOrLabel}`);
|
|
166
|
+
logger.log("");
|
|
167
|
+
logger.log("Authorized machines:");
|
|
168
|
+
for (const m of machines) {
|
|
169
|
+
logger.log(` ${m.fingerprint} ${m.label ? `(${m.label})` : ""}`);
|
|
170
|
+
}
|
|
171
|
+
throw new SpacesError("Machine not found.", "USER_ERROR", 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
logger.log("");
|
|
175
|
+
logger.success("Machine authorization revoked.");
|
|
176
|
+
logger.log("");
|
|
177
|
+
logger.log(` Fingerprint: ${chalk.cyan(removed.fingerprint)}`);
|
|
178
|
+
if (removed.label) {
|
|
179
|
+
logger.log(` Label: ${chalk.yellow(removed.label)}`);
|
|
180
|
+
}
|
|
181
|
+
logger.log("");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* List all authorized machines
|
|
186
|
+
*/
|
|
187
|
+
export async function listMachines(): Promise<void> {
|
|
188
|
+
const machines = getAuthorizedMachines();
|
|
189
|
+
|
|
190
|
+
if (machines.length === 0) {
|
|
191
|
+
logger.log("");
|
|
192
|
+
logger.info("No machines authorized.");
|
|
193
|
+
logger.log("");
|
|
194
|
+
logger.log("Authorize a machine:");
|
|
195
|
+
logger.log(" gssh relay authorize gssh-pub:... --label 'My Machine'");
|
|
196
|
+
logger.log("");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
logger.log("");
|
|
201
|
+
logger.bold("Authorized Machines:");
|
|
202
|
+
logger.log("");
|
|
203
|
+
|
|
204
|
+
// Header
|
|
205
|
+
const fpWidth = 20;
|
|
206
|
+
const labelWidth = 24;
|
|
207
|
+
const dateWidth = 12;
|
|
208
|
+
|
|
209
|
+
logger.dim(
|
|
210
|
+
"FINGERPRINT".padEnd(fpWidth) +
|
|
211
|
+
"LABEL".padEnd(labelWidth) +
|
|
212
|
+
"AUTHORIZED"
|
|
213
|
+
);
|
|
214
|
+
logger.dim("─".repeat(fpWidth + labelWidth + dateWidth));
|
|
215
|
+
|
|
216
|
+
// Entries
|
|
217
|
+
for (const m of machines) {
|
|
218
|
+
const fp = m.fingerprint.padEnd(fpWidth);
|
|
219
|
+
const label = (m.label || "-").substring(0, labelWidth - 1).padEnd(labelWidth);
|
|
220
|
+
const date = new Date(m.authorizedAt).toISOString().split("T")[0];
|
|
221
|
+
|
|
222
|
+
logger.log(chalk.cyan(fp) + label + chalk.dim(date));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
logger.log("");
|
|
226
|
+
logger.dim(`Total: ${machines.length} machine(s)`);
|
|
227
|
+
logger.log("");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Trusted Relay Commands (Machine-side)
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* List all trusted relays
|
|
236
|
+
*/
|
|
237
|
+
export async function listTrustedRelays(): Promise<void> {
|
|
238
|
+
const relays = getTrustedRelays();
|
|
239
|
+
|
|
240
|
+
if (relays.length === 0) {
|
|
241
|
+
logger.log("");
|
|
242
|
+
logger.info("No trusted relays.");
|
|
243
|
+
logger.log("");
|
|
244
|
+
logger.log("Connect to a relay to establish trust:");
|
|
245
|
+
logger.log(" gssh serve --relay wss://relay.example.com");
|
|
246
|
+
logger.log("");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
logger.log("");
|
|
251
|
+
logger.bold("Trusted Relays:");
|
|
252
|
+
logger.log("");
|
|
253
|
+
|
|
254
|
+
// Header
|
|
255
|
+
const urlWidth = 32;
|
|
256
|
+
const fpWidth = 20;
|
|
257
|
+
const labelWidth = 16;
|
|
258
|
+
|
|
259
|
+
logger.dim(
|
|
260
|
+
"URL".padEnd(urlWidth) +
|
|
261
|
+
"FINGERPRINT".padEnd(fpWidth) +
|
|
262
|
+
"LABEL"
|
|
263
|
+
);
|
|
264
|
+
logger.dim("─".repeat(urlWidth + fpWidth + labelWidth));
|
|
265
|
+
|
|
266
|
+
// Entries
|
|
267
|
+
for (const r of relays) {
|
|
268
|
+
const url = r.url.substring(0, urlWidth - 1).padEnd(urlWidth);
|
|
269
|
+
const fp = r.fingerprint.padEnd(fpWidth);
|
|
270
|
+
const label = (r.label || "-").substring(0, labelWidth - 1);
|
|
271
|
+
|
|
272
|
+
logger.log(chalk.cyan(url) + fp + chalk.dim(label));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
logger.log("");
|
|
276
|
+
logger.dim(`Total: ${relays.length} relay(s)`);
|
|
277
|
+
logger.log("");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Remove trust for a relay
|
|
282
|
+
*
|
|
283
|
+
* @param urlOrFingerprint - URL, fingerprint, or label of relay to untrust
|
|
284
|
+
*/
|
|
285
|
+
export async function untrustRelay(urlOrFingerprint: string): Promise<void> {
|
|
286
|
+
const removed = removeTrustedRelay(urlOrFingerprint);
|
|
287
|
+
|
|
288
|
+
if (!removed) {
|
|
289
|
+
const relays = getTrustedRelays();
|
|
290
|
+
|
|
291
|
+
if (relays.length === 0) {
|
|
292
|
+
throw new SpacesError("No relays are trusted.", "USER_ERROR", 1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
logger.error(`No relay found matching: ${urlOrFingerprint}`);
|
|
296
|
+
logger.log("");
|
|
297
|
+
logger.log("Trusted relays:");
|
|
298
|
+
for (const r of relays) {
|
|
299
|
+
logger.log(` ${r.url} (${r.fingerprint}${r.label ? `, ${r.label}` : ""})`);
|
|
300
|
+
}
|
|
301
|
+
throw new SpacesError("Relay not found.", "USER_ERROR", 1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
logger.log("");
|
|
305
|
+
logger.success("Relay trust removed.");
|
|
306
|
+
logger.log("");
|
|
307
|
+
logger.log(` URL: ${chalk.cyan(removed.url)}`);
|
|
308
|
+
logger.log(` Fingerprint: ${removed.fingerprint}`);
|
|
309
|
+
if (removed.label) {
|
|
310
|
+
logger.log(` Label: ${chalk.yellow(removed.label)}`);
|
|
311
|
+
}
|
|
312
|
+
logger.log("");
|
|
313
|
+
logger.dim("You will be prompted to trust this relay again on next connection.");
|
|
314
|
+
logger.log("");
|
|
315
|
+
}
|