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,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote session handler - processes browse and PTY commands
|
|
3
|
+
*
|
|
4
|
+
* Handles the encrypted messages between client and machine after X3DH handshake.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createFrame, openFrame } from "../tmux-lite/crypto/frames";
|
|
8
|
+
import { scanWorkspaces } from "./workspace-scanner";
|
|
9
|
+
import {
|
|
10
|
+
parseRemoteMessage,
|
|
11
|
+
serializeRemoteMessage,
|
|
12
|
+
type ClientToMachineMessage,
|
|
13
|
+
type MachineToClientMessage,
|
|
14
|
+
type SessionInfo,
|
|
15
|
+
} from "./protocol";
|
|
16
|
+
import type { SessionKeys, AccessType } from "../../types/identity.js";
|
|
17
|
+
|
|
18
|
+
// Import tmux-lite API for session management
|
|
19
|
+
import {
|
|
20
|
+
listSessions,
|
|
21
|
+
createSession,
|
|
22
|
+
killSession,
|
|
23
|
+
isServerRunning,
|
|
24
|
+
ensureServer,
|
|
25
|
+
getInbox,
|
|
26
|
+
clearInbox,
|
|
27
|
+
markInboxRead,
|
|
28
|
+
type Session,
|
|
29
|
+
} from "../tmux-lite/cli";
|
|
30
|
+
|
|
31
|
+
// Import project loading
|
|
32
|
+
import { loadProjects } from "../../tui/state";
|
|
33
|
+
|
|
34
|
+
// Import workspace removal
|
|
35
|
+
import { removeWorktree, deleteLocalBranch, getWorktreeInfo } from "../../core/git";
|
|
36
|
+
import { getProjectWorkspacesDir, getProjectBaseDir } from "../../core/config";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Session state for a connected client
|
|
40
|
+
*/
|
|
41
|
+
export type ClientState = "browsing" | "attached";
|
|
42
|
+
|
|
43
|
+
export interface RemoteClientSession {
|
|
44
|
+
connectionId: string;
|
|
45
|
+
state: ClientState;
|
|
46
|
+
sessionKeys: SessionKeys;
|
|
47
|
+
/** Access type granted to this client */
|
|
48
|
+
accessType?: AccessType;
|
|
49
|
+
/** For session-invite: the specific session ID access was granted to */
|
|
50
|
+
grantedSessionId?: string;
|
|
51
|
+
/** Attached tmux-lite session ID (set after attach_session) */
|
|
52
|
+
attachedSessionId?: string;
|
|
53
|
+
/** Path to tmux-lite session socket (set after attach_session) */
|
|
54
|
+
sessionSocketPath?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Permission Helpers
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if access type grants management permission
|
|
63
|
+
*/
|
|
64
|
+
function canManage(accessType: AccessType | undefined): boolean {
|
|
65
|
+
return accessType === 'full';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if client can attach to a specific session
|
|
70
|
+
*/
|
|
71
|
+
function canAttachSession(
|
|
72
|
+
accessType: AccessType | undefined,
|
|
73
|
+
grantedSessionId: string | undefined,
|
|
74
|
+
targetSessionId: string
|
|
75
|
+
): boolean {
|
|
76
|
+
if (accessType === 'full') return true;
|
|
77
|
+
if (accessType === 'session-invite') {
|
|
78
|
+
return grantedSessionId === targetSessionId;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remote session handler
|
|
85
|
+
*/
|
|
86
|
+
export class RemoteSessionHandler {
|
|
87
|
+
private tmuxLiteAvailable = false;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Initialize - check if tmux-lite is available
|
|
91
|
+
*/
|
|
92
|
+
async initialize(): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
this.tmuxLiteAvailable = await isServerRunning();
|
|
95
|
+
if (!this.tmuxLiteAvailable) {
|
|
96
|
+
// Try to start the server
|
|
97
|
+
await ensureServer();
|
|
98
|
+
this.tmuxLiteAvailable = true;
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.warn("[remote-session] tmux-lite not available:", e);
|
|
102
|
+
this.tmuxLiteAvailable = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle an encrypted message from a client
|
|
108
|
+
*
|
|
109
|
+
* @param session - Client session info
|
|
110
|
+
* @param encryptedData - Encrypted frame data
|
|
111
|
+
* @param sendResponse - Callback to send encrypted response
|
|
112
|
+
*/
|
|
113
|
+
async handleMessage(
|
|
114
|
+
session: RemoteClientSession,
|
|
115
|
+
encryptedData: Uint8Array,
|
|
116
|
+
sendResponse: (data: Uint8Array) => void
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
try {
|
|
119
|
+
// Decrypt the frame
|
|
120
|
+
const frame = await openFrame(encryptedData, session.sessionKeys.receiveKey);
|
|
121
|
+
if (!frame) {
|
|
122
|
+
console.error("[remote-session] Failed to decrypt frame");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse as JSON message
|
|
127
|
+
const json = new TextDecoder().decode(frame.data);
|
|
128
|
+
const msg = parseRemoteMessage(json);
|
|
129
|
+
|
|
130
|
+
if (!msg) {
|
|
131
|
+
console.error("[remote-session] Failed to parse message");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle based on message type
|
|
136
|
+
await this.processMessage(session, msg as ClientToMachineMessage, sendResponse);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error("[remote-session] Error handling message:", e);
|
|
139
|
+
await this.sendError(session, sendResponse, "INTERNAL_ERROR", "Failed to process message");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Process a client message
|
|
145
|
+
*/
|
|
146
|
+
private async processMessage(
|
|
147
|
+
session: RemoteClientSession,
|
|
148
|
+
msg: ClientToMachineMessage,
|
|
149
|
+
sendResponse: (data: Uint8Array) => void
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
switch (msg.type) {
|
|
152
|
+
case "list_workspaces":
|
|
153
|
+
await this.handleListWorkspaces(session, sendResponse);
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case "list_sessions":
|
|
157
|
+
await this.handleListSessions(session, msg.workspaceId, sendResponse);
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case "attach_session":
|
|
161
|
+
// Permission check for attach_session is done in handleAttachSession
|
|
162
|
+
// because it depends on whether creating new session or attaching existing
|
|
163
|
+
await this.handleAttachSession(session, msg, sendResponse);
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
// Note: resize, detach, and pty_input are handled in attached mode
|
|
167
|
+
// via client-session-manager using tmux-lite's SessionCtrl protocol,
|
|
168
|
+
// not through this JSON-RPC handler.
|
|
169
|
+
|
|
170
|
+
case "list_projects":
|
|
171
|
+
await this.handleListProjects(session, sendResponse);
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case "kill_session":
|
|
175
|
+
// Security: Requires management permission
|
|
176
|
+
if (!canManage(session.accessType)) {
|
|
177
|
+
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to kill sessions");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await this.handleKillSession(session, msg.sessionId, sendResponse);
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case "delete_workspace":
|
|
184
|
+
// Security: Requires management permission
|
|
185
|
+
if (!canManage(session.accessType)) {
|
|
186
|
+
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to delete workspaces");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await this.handleDeleteWorkspace(session, msg.projectName, msg.workspaceId, sendResponse);
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
case "get_inbox":
|
|
193
|
+
await this.handleGetInbox(session, sendResponse);
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case "clear_inbox":
|
|
197
|
+
await this.handleClearInbox(session, msg.id, sendResponse);
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case "mark_inbox_read":
|
|
201
|
+
await this.handleMarkInboxRead(session, msg.id, sendResponse);
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
default: {
|
|
205
|
+
// Exhaustiveness check - log unknown message types
|
|
206
|
+
const unknownMsg = msg as { type: string };
|
|
207
|
+
console.warn("[remote-session] Unknown message type:", unknownMsg.type);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handle list_workspaces request
|
|
214
|
+
*/
|
|
215
|
+
private async handleListWorkspaces(
|
|
216
|
+
session: RemoteClientSession,
|
|
217
|
+
sendResponse: (data: Uint8Array) => void
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
const workspaces = await scanWorkspaces();
|
|
220
|
+
|
|
221
|
+
// Add session counts from tmux-lite
|
|
222
|
+
if (this.tmuxLiteAvailable) {
|
|
223
|
+
try {
|
|
224
|
+
const sessions = await listSessions();
|
|
225
|
+
for (const workspace of workspaces) {
|
|
226
|
+
// Count sessions for this workspace by matching cwd.
|
|
227
|
+
// Note: Session cwd is set once at creation time and does NOT change
|
|
228
|
+
// as users navigate within the shell. This is intentional - we want to
|
|
229
|
+
// show sessions that were *created for* this workspace.
|
|
230
|
+
workspace.sessionCount = sessions.filter(s => s.cwd === workspace.path).length;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Ignore errors - just use 0 session counts
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await this.sendMessage(session, sendResponse, {
|
|
238
|
+
type: "workspace_list",
|
|
239
|
+
workspaces,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Handle list_sessions request
|
|
245
|
+
*/
|
|
246
|
+
private async handleListSessions(
|
|
247
|
+
session: RemoteClientSession,
|
|
248
|
+
workspaceId: string | undefined,
|
|
249
|
+
sendResponse: (data: Uint8Array) => void
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
let sessions: SessionInfo[] = [];
|
|
252
|
+
|
|
253
|
+
if (this.tmuxLiteAvailable) {
|
|
254
|
+
try {
|
|
255
|
+
const allSessions = await listSessions();
|
|
256
|
+
const workspaces = await scanWorkspaces();
|
|
257
|
+
|
|
258
|
+
// Build a map of workspace path -> workspace info
|
|
259
|
+
const workspacePathMap = new Map(workspaces.map(w => [w.path, w]));
|
|
260
|
+
|
|
261
|
+
sessions = allSessions
|
|
262
|
+
.filter(s => {
|
|
263
|
+
if (!workspaceId) return true;
|
|
264
|
+
// Filter by workspace using cwd matching
|
|
265
|
+
// Note: Session cwd is set once at creation time and does NOT change
|
|
266
|
+
// as users navigate within the shell.
|
|
267
|
+
const ws = workspacePathMap.get(s.cwd);
|
|
268
|
+
return ws?.id === workspaceId;
|
|
269
|
+
})
|
|
270
|
+
.map(s => {
|
|
271
|
+
// Find workspace info by cwd
|
|
272
|
+
const ws = workspacePathMap.get(s.cwd);
|
|
273
|
+
return {
|
|
274
|
+
id: s.id,
|
|
275
|
+
name: s.name,
|
|
276
|
+
workspaceId: ws?.id ?? "unknown",
|
|
277
|
+
attached: s.attached,
|
|
278
|
+
createdAt: s.createdAt,
|
|
279
|
+
processTitle: s.processTitle,
|
|
280
|
+
exitCode: s.exitCode,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.error("[remote-session] Failed to list sessions:", e);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await this.sendMessage(session, sendResponse, {
|
|
289
|
+
type: "session_list",
|
|
290
|
+
sessions,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Handle attach_session request
|
|
296
|
+
*/
|
|
297
|
+
private async handleAttachSession(
|
|
298
|
+
session: RemoteClientSession,
|
|
299
|
+
msg: { sessionId?: string; workspaceId?: string; sessionName?: string; cols?: number; rows?: number },
|
|
300
|
+
sendResponse: (data: Uint8Array) => void
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
console.log("[remote-session] handleAttachSession:", JSON.stringify(msg));
|
|
303
|
+
|
|
304
|
+
if (!this.tmuxLiteAvailable) {
|
|
305
|
+
await this.sendError(session, sendResponse, "UNAVAILABLE", "Session manager not available");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
let targetSession: Session | null = null;
|
|
311
|
+
|
|
312
|
+
// If no session ID, create new session in workspace
|
|
313
|
+
if (!msg.sessionId && msg.workspaceId) {
|
|
314
|
+
// Security: Creating new sessions requires full/manage access
|
|
315
|
+
if (!canManage(session.accessType)) {
|
|
316
|
+
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to create sessions");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Find the workspace path
|
|
321
|
+
const workspaces = await scanWorkspaces();
|
|
322
|
+
const workspace = workspaces.find(w => w.id === msg.workspaceId);
|
|
323
|
+
|
|
324
|
+
if (!workspace) {
|
|
325
|
+
await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Create session name: use provided name or auto-generate
|
|
330
|
+
let sessionName: string;
|
|
331
|
+
if (msg.sessionName) {
|
|
332
|
+
// Use provided name with project:workspace prefix
|
|
333
|
+
sessionName = `${workspace.projectName}:${workspace.id}:${msg.sessionName}`;
|
|
334
|
+
console.log(`[remote-session] Using provided session name: ${sessionName}`);
|
|
335
|
+
} else {
|
|
336
|
+
// Auto-generate: project:workspace:N
|
|
337
|
+
const sessions = await listSessions();
|
|
338
|
+
const existingCount = sessions.filter(s =>
|
|
339
|
+
s.name.startsWith(`${workspace.projectName}:${workspace.id}:`)
|
|
340
|
+
).length;
|
|
341
|
+
sessionName = `${workspace.projectName}:${workspace.id}:${existingCount + 1}`;
|
|
342
|
+
console.log(`[remote-session] Auto-generated session name: ${sessionName}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
targetSession = await createSession(sessionName, workspace.path);
|
|
346
|
+
console.log(`[remote-session] Created session: ${targetSession.name} (id: ${targetSession.id})`)
|
|
347
|
+
} else if (msg.sessionId) {
|
|
348
|
+
// Security: Check if client can attach to this session
|
|
349
|
+
if (!canAttachSession(session.accessType, session.grantedSessionId, msg.sessionId)) {
|
|
350
|
+
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Not authorized to attach to this session");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Find existing session
|
|
355
|
+
const sessions = await listSessions();
|
|
356
|
+
targetSession = sessions.find(s => s.id === msg.sessionId) ?? null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!targetSession) {
|
|
360
|
+
await this.sendError(session, sendResponse, "NOT_FOUND", "Session not found");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
session.state = "attached";
|
|
365
|
+
session.attachedSessionId = targetSession.id;
|
|
366
|
+
session.sessionSocketPath = targetSession.socketPath;
|
|
367
|
+
|
|
368
|
+
// Send confirmation - ClientSessionManager will connect to the socket
|
|
369
|
+
await this.sendMessage(session, sendResponse, {
|
|
370
|
+
type: "attached",
|
|
371
|
+
sessionId: targetSession.id,
|
|
372
|
+
sessionName: targetSession.name,
|
|
373
|
+
cols: msg.cols ?? 80,
|
|
374
|
+
rows: msg.rows ?? 24,
|
|
375
|
+
});
|
|
376
|
+
} catch (e) {
|
|
377
|
+
console.error("[remote-session] Failed to attach session:", e);
|
|
378
|
+
await this.sendError(session, sendResponse, "ATTACH_FAILED", "Failed to attach to session");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Handle list_projects request
|
|
384
|
+
*/
|
|
385
|
+
private async handleListProjects(
|
|
386
|
+
session: RemoteClientSession,
|
|
387
|
+
sendResponse: (data: Uint8Array) => void
|
|
388
|
+
): Promise<void> {
|
|
389
|
+
try {
|
|
390
|
+
const projects = loadProjects();
|
|
391
|
+
await this.sendMessage(session, sendResponse, {
|
|
392
|
+
type: "project_list",
|
|
393
|
+
projects: projects.map(p => ({
|
|
394
|
+
name: p.name,
|
|
395
|
+
repository: p.repository,
|
|
396
|
+
workspaceCount: p.workspaceCount,
|
|
397
|
+
isCurrent: p.isCurrent,
|
|
398
|
+
})),
|
|
399
|
+
});
|
|
400
|
+
} catch (e) {
|
|
401
|
+
console.error("[remote-session] Failed to list projects:", e);
|
|
402
|
+
await this.sendError(session, sendResponse, "LIST_FAILED", "Failed to list projects");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Handle kill_session request
|
|
408
|
+
*/
|
|
409
|
+
private async handleKillSession(
|
|
410
|
+
session: RemoteClientSession,
|
|
411
|
+
sessionId: string,
|
|
412
|
+
sendResponse: (data: Uint8Array) => void
|
|
413
|
+
): Promise<void> {
|
|
414
|
+
if (!this.tmuxLiteAvailable) {
|
|
415
|
+
await this.sendError(session, sendResponse, "UNAVAILABLE", "Session manager not available");
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
// Look up the session's workspaceId before killing
|
|
421
|
+
const sessions = await listSessions();
|
|
422
|
+
const workspaces = await scanWorkspaces();
|
|
423
|
+
const workspacePathMap = new Map(workspaces.map(w => [w.path, w]));
|
|
424
|
+
const targetSession = sessions.find(s => s.id === sessionId);
|
|
425
|
+
const workspace = targetSession ? workspacePathMap.get(targetSession.cwd) : undefined;
|
|
426
|
+
const workspaceId = workspace?.id ?? "unknown";
|
|
427
|
+
|
|
428
|
+
await killSession(sessionId);
|
|
429
|
+
// Wait a bit for the server to process the kill
|
|
430
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
431
|
+
await this.sendMessage(session, sendResponse, {
|
|
432
|
+
type: "session_killed",
|
|
433
|
+
sessionId,
|
|
434
|
+
workspaceId,
|
|
435
|
+
});
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.error("[remote-session] Failed to kill session:", e);
|
|
438
|
+
await this.sendError(session, sendResponse, "KILL_FAILED", "Failed to kill session");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Handle delete_workspace request
|
|
444
|
+
*/
|
|
445
|
+
private async handleDeleteWorkspace(
|
|
446
|
+
session: RemoteClientSession,
|
|
447
|
+
projectName: string,
|
|
448
|
+
workspaceId: string,
|
|
449
|
+
sendResponse: (data: Uint8Array) => void
|
|
450
|
+
): Promise<void> {
|
|
451
|
+
try {
|
|
452
|
+
const workspacesDir = getProjectWorkspacesDir(projectName);
|
|
453
|
+
const baseDir = getProjectBaseDir(projectName);
|
|
454
|
+
const workspacePath = `${workspacesDir}/${workspaceId}`;
|
|
455
|
+
|
|
456
|
+
// Get workspace info for branch name
|
|
457
|
+
const info = await getWorktreeInfo(workspacePath);
|
|
458
|
+
if (!info) {
|
|
459
|
+
await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Remove worktree
|
|
464
|
+
await removeWorktree(baseDir, workspacePath, true);
|
|
465
|
+
|
|
466
|
+
// Try to delete the local branch
|
|
467
|
+
try {
|
|
468
|
+
await deleteLocalBranch(baseDir, info.branch, true);
|
|
469
|
+
} catch {
|
|
470
|
+
// Branch deletion is best-effort
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
await this.sendMessage(session, sendResponse, {
|
|
474
|
+
type: "workspace_deleted",
|
|
475
|
+
workspaceId,
|
|
476
|
+
});
|
|
477
|
+
} catch (e) {
|
|
478
|
+
console.error("[remote-session] Failed to delete workspace:", e);
|
|
479
|
+
await this.sendError(session, sendResponse, "DELETE_FAILED", "Failed to delete workspace");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Handle get_inbox request
|
|
485
|
+
*/
|
|
486
|
+
private async handleGetInbox(
|
|
487
|
+
session: RemoteClientSession,
|
|
488
|
+
sendResponse: (data: Uint8Array) => void
|
|
489
|
+
): Promise<void> {
|
|
490
|
+
try {
|
|
491
|
+
const items = await getInbox();
|
|
492
|
+
const unreadCount = items.filter(i => !i.read).length;
|
|
493
|
+
await this.sendMessage(session, sendResponse, {
|
|
494
|
+
type: "inbox_list",
|
|
495
|
+
items,
|
|
496
|
+
unreadCount,
|
|
497
|
+
});
|
|
498
|
+
} catch (e) {
|
|
499
|
+
console.error("[remote-session] Failed to get inbox:", e);
|
|
500
|
+
await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to get inbox");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Handle clear_inbox request
|
|
506
|
+
*/
|
|
507
|
+
private async handleClearInbox(
|
|
508
|
+
session: RemoteClientSession,
|
|
509
|
+
id: string | undefined,
|
|
510
|
+
sendResponse: (data: Uint8Array) => void
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
try {
|
|
513
|
+
await clearInbox(id);
|
|
514
|
+
await this.sendMessage(session, sendResponse, {
|
|
515
|
+
type: "inbox_cleared",
|
|
516
|
+
id,
|
|
517
|
+
});
|
|
518
|
+
} catch (e) {
|
|
519
|
+
console.error("[remote-session] Failed to clear inbox:", e);
|
|
520
|
+
await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to clear inbox");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Handle mark_inbox_read request
|
|
526
|
+
*/
|
|
527
|
+
private async handleMarkInboxRead(
|
|
528
|
+
session: RemoteClientSession,
|
|
529
|
+
id: string,
|
|
530
|
+
sendResponse: (data: Uint8Array) => void
|
|
531
|
+
): Promise<void> {
|
|
532
|
+
try {
|
|
533
|
+
await markInboxRead(id);
|
|
534
|
+
await this.sendMessage(session, sendResponse, {
|
|
535
|
+
type: "inbox_marked_read",
|
|
536
|
+
id,
|
|
537
|
+
});
|
|
538
|
+
} catch (e) {
|
|
539
|
+
console.error("[remote-session] Failed to mark inbox read:", e);
|
|
540
|
+
await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to mark inbox item as read");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Send an encrypted message to client
|
|
546
|
+
*/
|
|
547
|
+
private async sendMessage(
|
|
548
|
+
session: RemoteClientSession,
|
|
549
|
+
sendResponse: (data: Uint8Array) => void,
|
|
550
|
+
msg: MachineToClientMessage
|
|
551
|
+
): Promise<void> {
|
|
552
|
+
const json = serializeRemoteMessage(msg);
|
|
553
|
+
const data = new TextEncoder().encode(json);
|
|
554
|
+
const frame = await createFrame(0, data, session.sessionKeys.sendKey);
|
|
555
|
+
sendResponse(frame);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Send an error message to client
|
|
560
|
+
*/
|
|
561
|
+
private async sendError(
|
|
562
|
+
session: RemoteClientSession,
|
|
563
|
+
sendResponse: (data: Uint8Array) => void,
|
|
564
|
+
code: string,
|
|
565
|
+
message: string
|
|
566
|
+
): Promise<void> {
|
|
567
|
+
await this.sendMessage(session, sendResponse, {
|
|
568
|
+
type: "error",
|
|
569
|
+
code,
|
|
570
|
+
message,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Cleanup
|
|
576
|
+
*/
|
|
577
|
+
async cleanup(): Promise<void> {
|
|
578
|
+
// No persistent connection to clean up with the new API
|
|
579
|
+
this.tmuxLiteAvailable = false;
|
|
580
|
+
}
|
|
581
|
+
}
|