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,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteMachineProvider - Remote Machine Access via Relay
|
|
3
|
+
*
|
|
4
|
+
* Implements MachineProvider interface for accessing remote machines
|
|
5
|
+
* through the relay server with encrypted communication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
MachineProvider,
|
|
10
|
+
CreateSessionOptions,
|
|
11
|
+
AttachSessionOptions,
|
|
12
|
+
} from './MachineProvider.js';
|
|
13
|
+
import type {
|
|
14
|
+
MachineInfo,
|
|
15
|
+
Project,
|
|
16
|
+
Workspace,
|
|
17
|
+
InboxItem,
|
|
18
|
+
SessionStream,
|
|
19
|
+
} from '../types.js';
|
|
20
|
+
|
|
21
|
+
// Use dynamic import for WebSocket to support both Node and browser
|
|
22
|
+
// In Node, we use the 'ws' package; in browser, we use native WebSocket
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Common WebSocket interface for both Node.js (ws package) and browser environments
|
|
26
|
+
*/
|
|
27
|
+
interface WebSocketLike {
|
|
28
|
+
send(data: string): void;
|
|
29
|
+
close(): void;
|
|
30
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
31
|
+
off(event: string, handler: (...args: unknown[]) => void): void;
|
|
32
|
+
readyState: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Adapt the 'ws' package WebSocket to our common interface.
|
|
37
|
+
* The 'ws' package uses EventEmitter-style on/off which matches our interface.
|
|
38
|
+
*/
|
|
39
|
+
function adaptWebSocket(ws: {
|
|
40
|
+
send(data: string): void;
|
|
41
|
+
close(): void;
|
|
42
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
43
|
+
off(event: string, handler: (...args: unknown[]) => void): void;
|
|
44
|
+
readyState: number;
|
|
45
|
+
}): WebSocketLike {
|
|
46
|
+
return ws;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const OPEN = 1;
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Types
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
export interface RemoteMachineProviderConfig {
|
|
56
|
+
relayUrl: string;
|
|
57
|
+
/** Optional identity for the TUI client (generated if not provided) */
|
|
58
|
+
clientIdentityId?: string;
|
|
59
|
+
/** Machine ID to connect to (selected from list) */
|
|
60
|
+
machineId?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RelayMessage {
|
|
64
|
+
type: string;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Provider Implementation
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
export class RemoteMachineProvider implements MachineProvider {
|
|
73
|
+
private ws: WebSocketLike | null = null;
|
|
74
|
+
private config: RemoteMachineProviderConfig;
|
|
75
|
+
private messageHandlers = new Map<string, (msg: RelayMessage) => void>();
|
|
76
|
+
private messageQueue: RelayMessage[] = [];
|
|
77
|
+
private connected = false;
|
|
78
|
+
private machineId: string | null = null;
|
|
79
|
+
|
|
80
|
+
constructor(config: RemoteMachineProviderConfig) {
|
|
81
|
+
this.config = config;
|
|
82
|
+
this.machineId = config.machineId ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Connect to relay server
|
|
87
|
+
*/
|
|
88
|
+
async connect(): Promise<void> {
|
|
89
|
+
// Dynamic import for ws module (Node.js)
|
|
90
|
+
const { default: WebSocket } = await import('ws');
|
|
91
|
+
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const url = new URL(this.config.relayUrl);
|
|
94
|
+
url.searchParams.set('role', 'client');
|
|
95
|
+
// Note: Authentication is now via challenge-response, not URL tokens
|
|
96
|
+
if (this.config.clientIdentityId) {
|
|
97
|
+
url.searchParams.set('clientId', this.config.clientIdentityId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ws = adaptWebSocket(new WebSocket(url.toString()));
|
|
101
|
+
this.ws = ws;
|
|
102
|
+
|
|
103
|
+
ws.on('open', () => {
|
|
104
|
+
this.connected = true;
|
|
105
|
+
// Send any queued messages
|
|
106
|
+
for (const msg of this.messageQueue) {
|
|
107
|
+
this.send(msg);
|
|
108
|
+
}
|
|
109
|
+
this.messageQueue = [];
|
|
110
|
+
resolve();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
ws.on('message', (data: unknown) => {
|
|
114
|
+
try {
|
|
115
|
+
const dataStr = typeof data === 'string' ? data : String(data);
|
|
116
|
+
const msg = JSON.parse(dataStr) as RelayMessage;
|
|
117
|
+
this.handleMessage(msg);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error('Failed to parse relay message:', e);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
ws.on('error', (err: unknown) => {
|
|
124
|
+
reject(err);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
ws.on('close', () => {
|
|
128
|
+
this.connected = false;
|
|
129
|
+
this.ws = null;
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Send message to relay
|
|
136
|
+
*/
|
|
137
|
+
private send(msg: RelayMessage): void {
|
|
138
|
+
if (!this.connected || !this.ws) {
|
|
139
|
+
this.messageQueue.push(msg);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.ws.send(JSON.stringify(msg));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Handle incoming relay message
|
|
147
|
+
*/
|
|
148
|
+
private handleMessage(msg: RelayMessage): void {
|
|
149
|
+
const handler = this.messageHandlers.get(msg.type);
|
|
150
|
+
if (handler) {
|
|
151
|
+
handler(msg);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Send request and wait for response
|
|
157
|
+
*/
|
|
158
|
+
private async request<T>(type: string, payload: Record<string, unknown> = {}): Promise<T> {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const responseType = `${type}_response`;
|
|
161
|
+
const errorType = `${type}_error`;
|
|
162
|
+
|
|
163
|
+
const cleanup = () => {
|
|
164
|
+
this.messageHandlers.delete(responseType);
|
|
165
|
+
this.messageHandlers.delete(errorType);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
this.messageHandlers.set(responseType, (msg) => {
|
|
169
|
+
cleanup();
|
|
170
|
+
resolve(msg as T);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.messageHandlers.set(errorType, (msg) => {
|
|
174
|
+
cleanup();
|
|
175
|
+
reject(new Error((msg.message as string) || 'Request failed'));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Also handle generic error
|
|
179
|
+
this.messageHandlers.set('error', (msg) => {
|
|
180
|
+
cleanup();
|
|
181
|
+
reject(new Error((msg.message as string) || 'Request failed'));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this.send({
|
|
185
|
+
type,
|
|
186
|
+
machineId: this.machineId,
|
|
187
|
+
...payload,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Set the target machine ID
|
|
194
|
+
*/
|
|
195
|
+
setMachineId(machineId: string): void {
|
|
196
|
+
this.machineId = machineId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// MachineProvider Interface
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
async getMachineInfo(): Promise<MachineInfo> {
|
|
204
|
+
if (!this.machineId) {
|
|
205
|
+
throw new Error('No machine selected');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const response = await this.request<{ machine: MachineInfo }>('get_machine_info', {
|
|
209
|
+
machineId: this.machineId,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return response.machine;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async listProjects(): Promise<Project[]> {
|
|
216
|
+
if (!this.machineId) {
|
|
217
|
+
throw new Error('No machine selected');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const response = await this.request<{ projects: Project[] }>('list_projects');
|
|
221
|
+
return response.projects;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async listWorkspaces(projectName: string): Promise<Workspace[]> {
|
|
225
|
+
if (!this.machineId) {
|
|
226
|
+
throw new Error('No machine selected');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const response = await this.request<{ workspaces: Workspace[] }>('list_workspaces', {
|
|
230
|
+
projectName,
|
|
231
|
+
});
|
|
232
|
+
return response.workspaces;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async createSession(
|
|
236
|
+
projectName: string,
|
|
237
|
+
workspaceName: string,
|
|
238
|
+
options?: CreateSessionOptions
|
|
239
|
+
): Promise<string> {
|
|
240
|
+
if (!this.machineId) {
|
|
241
|
+
throw new Error('No machine selected');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const response = await this.request<{ sessionId: string }>('create_session', {
|
|
245
|
+
projectName,
|
|
246
|
+
workspaceName,
|
|
247
|
+
sessionName: options?.sessionName,
|
|
248
|
+
shell: options?.shell,
|
|
249
|
+
});
|
|
250
|
+
return response.sessionId;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async attachSession(
|
|
254
|
+
sessionId: string,
|
|
255
|
+
options: AttachSessionOptions
|
|
256
|
+
): Promise<SessionStream> {
|
|
257
|
+
if (!this.machineId) {
|
|
258
|
+
throw new Error('No machine selected');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Create a session stream that communicates over the relay
|
|
262
|
+
const stream = new RemoteSessionStream(
|
|
263
|
+
this.ws!,
|
|
264
|
+
sessionId,
|
|
265
|
+
this.machineId,
|
|
266
|
+
options
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await stream.attach();
|
|
270
|
+
return stream;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async detachSession(sessionId: string): Promise<void> {
|
|
274
|
+
if (!this.machineId) {
|
|
275
|
+
throw new Error('No machine selected');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await this.request('detach_session', { sessionId });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async getInbox(): Promise<InboxItem[]> {
|
|
282
|
+
if (!this.machineId) {
|
|
283
|
+
throw new Error('No machine selected');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const response = await this.request<{ items: InboxItem[] }>('get_inbox');
|
|
287
|
+
return response.items;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async markInboxRead(itemId: string): Promise<void> {
|
|
291
|
+
if (!this.machineId) {
|
|
292
|
+
throw new Error('No machine selected');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await this.request('mark_inbox_read', { itemId });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async clearInbox(): Promise<void> {
|
|
299
|
+
if (!this.machineId) {
|
|
300
|
+
throw new Error('No machine selected');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await this.request('clear_inbox');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
dispose(): void {
|
|
307
|
+
if (this.ws) {
|
|
308
|
+
this.ws.close();
|
|
309
|
+
this.ws = null;
|
|
310
|
+
}
|
|
311
|
+
this.connected = false;
|
|
312
|
+
this.messageHandlers.clear();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// Remote Session Stream
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
class RemoteSessionStream implements SessionStream {
|
|
321
|
+
private ws: WebSocketLike;
|
|
322
|
+
private sessionId: string;
|
|
323
|
+
private machineId: string;
|
|
324
|
+
private options: AttachSessionOptions;
|
|
325
|
+
private dataHandler: ((data: Uint8Array) => void) | null = null;
|
|
326
|
+
private closeHandler: ((exitCode?: number) => void) | null = null;
|
|
327
|
+
private messageListener: ((data: unknown) => void) | null = null;
|
|
328
|
+
|
|
329
|
+
constructor(
|
|
330
|
+
ws: WebSocketLike,
|
|
331
|
+
sessionId: string,
|
|
332
|
+
machineId: string,
|
|
333
|
+
options: AttachSessionOptions
|
|
334
|
+
) {
|
|
335
|
+
this.ws = ws;
|
|
336
|
+
this.sessionId = sessionId;
|
|
337
|
+
this.machineId = machineId;
|
|
338
|
+
this.options = options;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async attach(): Promise<void> {
|
|
342
|
+
// Set up message listener for session data
|
|
343
|
+
this.messageListener = (data: unknown) => {
|
|
344
|
+
try {
|
|
345
|
+
const dataStr = typeof data === 'string' ? data : String(data);
|
|
346
|
+
const msg = JSON.parse(dataStr);
|
|
347
|
+
if (msg.type === 'session_data' && msg.sessionId === this.sessionId) {
|
|
348
|
+
if (this.dataHandler && msg.data) {
|
|
349
|
+
const decoded = Buffer.from(msg.data, 'base64');
|
|
350
|
+
this.dataHandler(new Uint8Array(decoded));
|
|
351
|
+
}
|
|
352
|
+
} else if (msg.type === 'session_closed' && msg.sessionId === this.sessionId) {
|
|
353
|
+
if (this.closeHandler) {
|
|
354
|
+
this.closeHandler(msg.exitCode);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
// Ignore parse errors
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
this.ws.on('message', this.messageListener);
|
|
363
|
+
|
|
364
|
+
// Send attach request
|
|
365
|
+
this.ws.send(JSON.stringify({
|
|
366
|
+
type: 'attach_session',
|
|
367
|
+
machineId: this.machineId,
|
|
368
|
+
sessionId: this.sessionId,
|
|
369
|
+
cols: this.options.cols,
|
|
370
|
+
rows: this.options.rows,
|
|
371
|
+
force: this.options.force ?? false,
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
write(data: Uint8Array): void {
|
|
376
|
+
const encoded = Buffer.from(data).toString('base64');
|
|
377
|
+
this.ws.send(JSON.stringify({
|
|
378
|
+
type: 'session_input',
|
|
379
|
+
machineId: this.machineId,
|
|
380
|
+
sessionId: this.sessionId,
|
|
381
|
+
data: encoded,
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
resize(cols: number, rows: number): void {
|
|
386
|
+
this.ws.send(JSON.stringify({
|
|
387
|
+
type: 'session_resize',
|
|
388
|
+
machineId: this.machineId,
|
|
389
|
+
sessionId: this.sessionId,
|
|
390
|
+
cols,
|
|
391
|
+
rows,
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
detach(): void {
|
|
396
|
+
this.ws.send(JSON.stringify({
|
|
397
|
+
type: 'detach_session',
|
|
398
|
+
machineId: this.machineId,
|
|
399
|
+
sessionId: this.sessionId,
|
|
400
|
+
}));
|
|
401
|
+
this.cleanup();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
close(): void {
|
|
405
|
+
this.ws.send(JSON.stringify({
|
|
406
|
+
type: 'close_session',
|
|
407
|
+
machineId: this.machineId,
|
|
408
|
+
sessionId: this.sessionId,
|
|
409
|
+
}));
|
|
410
|
+
this.cleanup();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
onData(handler: (data: Uint8Array) => void): void {
|
|
414
|
+
this.dataHandler = handler;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
onClose(handler: (exitCode?: number) => void): void {
|
|
418
|
+
this.closeHandler = handler;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private cleanup(): void {
|
|
422
|
+
if (this.messageListener) {
|
|
423
|
+
this.ws.off('message', this.messageListener);
|
|
424
|
+
this.messageListener = null;
|
|
425
|
+
}
|
|
426
|
+
this.dataHandler = null;
|
|
427
|
+
this.closeHandler = null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ============================================================================
|
|
432
|
+
// Factory Function
|
|
433
|
+
// ============================================================================
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Create a RemoteMachineProvider connected to the relay
|
|
437
|
+
*/
|
|
438
|
+
export async function createRemoteMachineProvider(
|
|
439
|
+
config: RemoteMachineProviderConfig
|
|
440
|
+
): Promise<RemoteMachineProvider> {
|
|
441
|
+
const provider = new RemoteMachineProvider(config);
|
|
442
|
+
await provider.connect();
|
|
443
|
+
return provider;
|
|
444
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
MachineProvider,
|
|
7
|
+
CreateSessionOptions,
|
|
8
|
+
AttachSessionOptions,
|
|
9
|
+
MachineProviderEvent,
|
|
10
|
+
MachineProviderEventHandler,
|
|
11
|
+
EventedMachineProvider,
|
|
12
|
+
} from './MachineProvider.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
LocalMachineProvider,
|
|
16
|
+
getLocalMachineProvider,
|
|
17
|
+
} from './LocalMachineProvider.js';
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
RemoteMachineProviderConfig,
|
|
21
|
+
} from './RemoteMachineProvider.js';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
RemoteMachineProvider,
|
|
25
|
+
createRemoteMachineProvider,
|
|
26
|
+
} from './RemoteMachineProvider.js';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for TUI and Web components
|
|
3
|
+
*
|
|
4
|
+
* These types are platform-agnostic and used by both interfaces.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Machine Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/** Machine connection status */
|
|
12
|
+
export type MachineStatus = 'connected' | 'connecting' | 'disconnected' | 'error';
|
|
13
|
+
|
|
14
|
+
/** Machine info for display */
|
|
15
|
+
export interface MachineInfo {
|
|
16
|
+
/** Unique machine ID */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Display label */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Whether this is the local machine */
|
|
21
|
+
isLocal: boolean;
|
|
22
|
+
/** Connection status */
|
|
23
|
+
status: MachineStatus;
|
|
24
|
+
/** Error message if status is 'error' */
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Project Types
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/** Project state for display */
|
|
33
|
+
export interface Project {
|
|
34
|
+
/** Project name */
|
|
35
|
+
name: string;
|
|
36
|
+
/** GitHub repository (e.g., "owner/repo") */
|
|
37
|
+
repository: string;
|
|
38
|
+
/** Number of workspaces */
|
|
39
|
+
workspaceCount: number;
|
|
40
|
+
/** Whether this is the currently selected project */
|
|
41
|
+
isCurrent: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Workspace Types
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/** Session within a workspace */
|
|
49
|
+
export interface WorkspaceSession {
|
|
50
|
+
/** Session ID */
|
|
51
|
+
id: string;
|
|
52
|
+
/** Session name */
|
|
53
|
+
name: string;
|
|
54
|
+
/** Whether a client is attached */
|
|
55
|
+
attached: boolean;
|
|
56
|
+
/** Creation timestamp */
|
|
57
|
+
createdAt: number;
|
|
58
|
+
/** Current process title */
|
|
59
|
+
processTitle?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Workspace state for display */
|
|
63
|
+
export interface Workspace {
|
|
64
|
+
/** Workspace name */
|
|
65
|
+
name: string;
|
|
66
|
+
/** Full path to workspace directory */
|
|
67
|
+
path: string;
|
|
68
|
+
/** Git branch name */
|
|
69
|
+
branch: string;
|
|
70
|
+
/** Commits ahead of remote */
|
|
71
|
+
ahead: number;
|
|
72
|
+
/** Commits behind remote */
|
|
73
|
+
behind: number;
|
|
74
|
+
/** Number of uncommitted changes */
|
|
75
|
+
uncommittedChanges: number;
|
|
76
|
+
/** Last commit date */
|
|
77
|
+
lastCommitDate: Date;
|
|
78
|
+
/** Whether workspace is stale (no recent activity) */
|
|
79
|
+
isStale: boolean;
|
|
80
|
+
/** Active sessions in this workspace */
|
|
81
|
+
sessions: WorkspaceSession[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Inbox Types
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
/** Inbox notification types */
|
|
89
|
+
export type InboxItemType = 'bell' | 'exit' | 'title' | 'idle';
|
|
90
|
+
|
|
91
|
+
/** Inbox notification item */
|
|
92
|
+
export interface InboxItem {
|
|
93
|
+
/** Unique ID */
|
|
94
|
+
id: string;
|
|
95
|
+
/** Session ID that generated the notification */
|
|
96
|
+
sessionId: string;
|
|
97
|
+
/** Session name */
|
|
98
|
+
sessionName: string;
|
|
99
|
+
/** Notification type */
|
|
100
|
+
type: InboxItemType;
|
|
101
|
+
/** Timestamp */
|
|
102
|
+
timestamp: number;
|
|
103
|
+
/** Whether the item has been read */
|
|
104
|
+
read: boolean;
|
|
105
|
+
/** Context/message content */
|
|
106
|
+
context: string;
|
|
107
|
+
/** Process title when notification occurred */
|
|
108
|
+
processTitle?: string;
|
|
109
|
+
/** Exit code (for exit type) */
|
|
110
|
+
exitCode?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Session Stream Types
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/** Stream for terminal I/O */
|
|
118
|
+
export interface SessionStream {
|
|
119
|
+
/** Send data to the session */
|
|
120
|
+
write(data: Uint8Array): void;
|
|
121
|
+
/** Resize the terminal */
|
|
122
|
+
resize(cols: number, rows: number): void;
|
|
123
|
+
/** Detach from the session */
|
|
124
|
+
detach(): void;
|
|
125
|
+
/** Close the stream */
|
|
126
|
+
close(): void;
|
|
127
|
+
/** Register data handler */
|
|
128
|
+
onData(handler: (data: Uint8Array) => void): void;
|
|
129
|
+
/** Register close handler */
|
|
130
|
+
onClose(handler: (exitCode?: number) => void): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Navigation Types
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
/** Navigation location */
|
|
138
|
+
export type NavigationLocation =
|
|
139
|
+
| { screen: 'machines' }
|
|
140
|
+
| { screen: 'projects'; machineId: string }
|
|
141
|
+
| { screen: 'workspaces'; machineId: string; projectName: string }
|
|
142
|
+
| { screen: 'session'; machineId: string; projectName: string; workspaceName: string; sessionId: string };
|
|
143
|
+
|
|
144
|
+
/** Panel focus */
|
|
145
|
+
export type PanelFocus = 'machines' | 'projects' | 'workspaces' | 'inbox';
|