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,199 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Client - connects to router, attaches to session
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun client.ts [project] [workspace]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { select } from "@inquirer/prompts";
|
|
9
|
+
import {
|
|
10
|
+
ROUTER_SOCKET,
|
|
11
|
+
type RouterCommand,
|
|
12
|
+
type RouterResponse,
|
|
13
|
+
type SessionInfo,
|
|
14
|
+
type AttachMode,
|
|
15
|
+
encodeControl,
|
|
16
|
+
isControl,
|
|
17
|
+
decodeControl,
|
|
18
|
+
type SessionEvent,
|
|
19
|
+
} from "./protocol";
|
|
20
|
+
|
|
21
|
+
const [project = "test", workspace = "default"] = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
// Connect to router
|
|
24
|
+
async function routerCommand(cmd: RouterCommand): Promise<RouterResponse> {
|
|
25
|
+
return new Promise(async (resolve, reject) => {
|
|
26
|
+
const socket = await Bun.connect({
|
|
27
|
+
unix: ROUTER_SOCKET,
|
|
28
|
+
socket: {
|
|
29
|
+
data(socket, data) {
|
|
30
|
+
resolve(JSON.parse(data.toString()));
|
|
31
|
+
socket.end();
|
|
32
|
+
},
|
|
33
|
+
error(socket, error) {
|
|
34
|
+
reject(error);
|
|
35
|
+
},
|
|
36
|
+
connectError(socket, error) {
|
|
37
|
+
reject(new Error("Router not running. Start with: bun router.ts"));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
socket.write(JSON.stringify(cmd));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get or create session
|
|
47
|
+
console.log(`Connecting to ${project}/${workspace}...`);
|
|
48
|
+
|
|
49
|
+
let session: SessionInfo;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
let response = await routerCommand({
|
|
53
|
+
type: "create",
|
|
54
|
+
project,
|
|
55
|
+
workspace,
|
|
56
|
+
cwd: process.cwd()
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Handle already-attached case
|
|
60
|
+
if (response.type === "already-attached") {
|
|
61
|
+
console.log(`\nSession "${project}/${workspace}" is already attached.\n`);
|
|
62
|
+
|
|
63
|
+
const choice = await select({
|
|
64
|
+
message: "What would you like to do?",
|
|
65
|
+
choices: [
|
|
66
|
+
{ value: "take-over", name: "Take over (disconnect other client)" },
|
|
67
|
+
{ value: "new", name: "Create new session for this workspace" },
|
|
68
|
+
{ value: "cancel", name: "Cancel" },
|
|
69
|
+
]
|
|
70
|
+
}) as AttachMode;
|
|
71
|
+
|
|
72
|
+
if (choice === "cancel") {
|
|
73
|
+
console.log("Cancelled.");
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (choice === "take-over") {
|
|
78
|
+
// Attach with take-over mode
|
|
79
|
+
response = await routerCommand({
|
|
80
|
+
type: "attach",
|
|
81
|
+
sessionId: response.session.id,
|
|
82
|
+
mode: "take-over"
|
|
83
|
+
});
|
|
84
|
+
} else if (choice === "new") {
|
|
85
|
+
// Force create new session (kill old one first, then create)
|
|
86
|
+
await routerCommand({ type: "kill", sessionId: response.session.id });
|
|
87
|
+
response = await routerCommand({
|
|
88
|
+
type: "create",
|
|
89
|
+
project,
|
|
90
|
+
workspace,
|
|
91
|
+
cwd: process.cwd()
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (response.type === "error") {
|
|
97
|
+
console.error("Error:", response.message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (response.type !== "created") {
|
|
102
|
+
console.error("Unexpected response:", response);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
session = response.session;
|
|
107
|
+
} catch (e: any) {
|
|
108
|
+
console.error(e.message);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`Attached to session ${session.id}`);
|
|
113
|
+
console.log("Press Ctrl+D to detach\n");
|
|
114
|
+
|
|
115
|
+
// Connect to session
|
|
116
|
+
const sessionSocket = await Bun.connect({
|
|
117
|
+
unix: session.socketPath,
|
|
118
|
+
socket: {
|
|
119
|
+
data(socket, data) {
|
|
120
|
+
const buf = Buffer.from(data);
|
|
121
|
+
|
|
122
|
+
if (isControl(buf)) {
|
|
123
|
+
const event = decodeControl(buf) as SessionEvent;
|
|
124
|
+
|
|
125
|
+
switch (event.type) {
|
|
126
|
+
case "attached":
|
|
127
|
+
// Replay scrollback
|
|
128
|
+
if (event.scrollback) {
|
|
129
|
+
const scrollback = Buffer.from(event.scrollback, "base64");
|
|
130
|
+
process.stdout.write(scrollback);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case "exited":
|
|
135
|
+
process.stdin.setRawMode(false);
|
|
136
|
+
console.log(`\nSession exited with code ${event.code}`);
|
|
137
|
+
process.exit(event.code);
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case "kicked":
|
|
141
|
+
process.stdin.setRawMode(false);
|
|
142
|
+
console.log("\n\nAnother client took over this session.");
|
|
143
|
+
process.exit(0);
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "pong":
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// PTY output - write to terminal
|
|
151
|
+
process.stdout.write(buf);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
close() {
|
|
156
|
+
process.stdin.setRawMode(false);
|
|
157
|
+
console.log("\nDisconnected from session");
|
|
158
|
+
process.exit(0);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
error(socket, error) {
|
|
162
|
+
process.stdin.setRawMode(false);
|
|
163
|
+
console.error("\nSession error:", error.message);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Send initial resize
|
|
170
|
+
sessionSocket.write(encodeControl({
|
|
171
|
+
type: "resize",
|
|
172
|
+
cols: process.stdout.columns || 80,
|
|
173
|
+
rows: process.stdout.rows || 24
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
// Handle terminal resize
|
|
177
|
+
process.stdout.on("resize", () => {
|
|
178
|
+
sessionSocket.write(encodeControl({
|
|
179
|
+
type: "resize",
|
|
180
|
+
cols: process.stdout.columns,
|
|
181
|
+
rows: process.stdout.rows
|
|
182
|
+
}));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Forward stdin to session
|
|
186
|
+
process.stdin.setRawMode(true);
|
|
187
|
+
process.stdin.resume();
|
|
188
|
+
|
|
189
|
+
for await (const chunk of process.stdin) {
|
|
190
|
+
// Check for Ctrl+D (detach)
|
|
191
|
+
if (chunk[0] === 4) {
|
|
192
|
+
sessionSocket.write(encodeControl({ type: "detach" }));
|
|
193
|
+
process.stdin.setRawMode(false);
|
|
194
|
+
console.log("\nDetached (session still running)");
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
sessionSocket.write(chunk);
|
|
199
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared protocol between router, sessions, and clients
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const ROUTER_SOCKET = "/tmp/spaces-router.sock";
|
|
6
|
+
export const SESSION_SOCKET_PREFIX = "/tmp/spaces-session-";
|
|
7
|
+
|
|
8
|
+
// Router commands (JSON over socket)
|
|
9
|
+
export type RouterCommand =
|
|
10
|
+
| { type: "list" }
|
|
11
|
+
| { type: "create"; project: string; workspace: string; cwd: string }
|
|
12
|
+
| { type: "attach"; sessionId: string; mode?: AttachMode }
|
|
13
|
+
| { type: "kill"; sessionId: string }
|
|
14
|
+
| { type: "kick"; sessionId: string }; // Disconnect current client
|
|
15
|
+
|
|
16
|
+
export type RouterResponse =
|
|
17
|
+
| { type: "sessions"; sessions: SessionInfo[] }
|
|
18
|
+
| { type: "created"; session: SessionInfo }
|
|
19
|
+
| { type: "already-attached"; session: SessionInfo } // Session exists but has a client
|
|
20
|
+
| { type: "error"; message: string }
|
|
21
|
+
| { type: "ok" };
|
|
22
|
+
|
|
23
|
+
export interface SessionInfo {
|
|
24
|
+
id: string;
|
|
25
|
+
project: string;
|
|
26
|
+
workspace: string;
|
|
27
|
+
socketPath: string;
|
|
28
|
+
pid: number;
|
|
29
|
+
attached: boolean; // true if a client is connected
|
|
30
|
+
createdAt: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// When attaching to an already-attached session
|
|
34
|
+
export type AttachMode =
|
|
35
|
+
| "take-over" // Disconnect existing client, you take over
|
|
36
|
+
| "new" // Create a new session for the same workspace
|
|
37
|
+
| "cancel"; // Abort
|
|
38
|
+
|
|
39
|
+
// Session protocol (binary + JSON control)
|
|
40
|
+
// Control messages start with 0x00, data is raw bytes
|
|
41
|
+
export const CONTROL_PREFIX = 0x00;
|
|
42
|
+
|
|
43
|
+
export type SessionControl =
|
|
44
|
+
| { type: "resize"; cols: number; rows: number }
|
|
45
|
+
| { type: "detach" }
|
|
46
|
+
| { type: "ping" };
|
|
47
|
+
|
|
48
|
+
export type SessionEvent =
|
|
49
|
+
| { type: "attached"; scrollback: string }
|
|
50
|
+
| { type: "exited"; code: number }
|
|
51
|
+
| { type: "kicked" } // Another client took over
|
|
52
|
+
| { type: "pong" };
|
|
53
|
+
|
|
54
|
+
// Helper to encode control message
|
|
55
|
+
export function encodeControl(msg: SessionControl | SessionEvent): Buffer {
|
|
56
|
+
const json = JSON.stringify(msg);
|
|
57
|
+
const buf = Buffer.alloc(1 + 4 + json.length);
|
|
58
|
+
buf[0] = CONTROL_PREFIX;
|
|
59
|
+
buf.writeUInt32BE(json.length, 1);
|
|
60
|
+
buf.write(json, 5);
|
|
61
|
+
return buf;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Helper to check if data is control message
|
|
65
|
+
export function isControl(data: Buffer): boolean {
|
|
66
|
+
return data[0] === CONTROL_PREFIX;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Helper to decode control message
|
|
70
|
+
export function decodeControl(data: Buffer): SessionControl | SessionEvent {
|
|
71
|
+
const len = data.readUInt32BE(1);
|
|
72
|
+
const json = data.subarray(5, 5 + len).toString();
|
|
73
|
+
return JSON.parse(json);
|
|
74
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Router - manages session lifecycle
|
|
4
|
+
* Always running, spawns sessions on demand
|
|
5
|
+
*
|
|
6
|
+
* Usage: bun router.ts
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { unlinkSync, existsSync } from "fs";
|
|
10
|
+
import { spawn } from "bun";
|
|
11
|
+
import {
|
|
12
|
+
ROUTER_SOCKET,
|
|
13
|
+
SESSION_SOCKET_PREFIX,
|
|
14
|
+
type RouterCommand,
|
|
15
|
+
type RouterResponse,
|
|
16
|
+
type SessionInfo,
|
|
17
|
+
} from "./protocol";
|
|
18
|
+
|
|
19
|
+
// Clean up existing socket
|
|
20
|
+
try { unlinkSync(ROUTER_SOCKET); } catch {}
|
|
21
|
+
|
|
22
|
+
// Active sessions
|
|
23
|
+
const sessions = new Map<string, {
|
|
24
|
+
info: SessionInfo;
|
|
25
|
+
proc: Bun.Subprocess;
|
|
26
|
+
stdin: WritableStream<Uint8Array>;
|
|
27
|
+
}>();
|
|
28
|
+
|
|
29
|
+
// Generate session ID
|
|
30
|
+
function genId(): string {
|
|
31
|
+
return Math.random().toString(36).substring(2, 10);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Find session by project/workspace
|
|
35
|
+
function findSession(project: string, workspace: string) {
|
|
36
|
+
for (const [id, session] of sessions) {
|
|
37
|
+
if (session.info.project === project && session.info.workspace === workspace) {
|
|
38
|
+
return session;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Spawn a new session
|
|
45
|
+
async function createSession(project: string, workspace: string, cwd: string): Promise<SessionInfo> {
|
|
46
|
+
const id = genId();
|
|
47
|
+
const socketPath = `${SESSION_SOCKET_PREFIX}${id}.sock`;
|
|
48
|
+
|
|
49
|
+
const proc = spawn({
|
|
50
|
+
cmd: ["bun", "run", `${import.meta.dir}/session.ts`, socketPath, cwd, project, workspace],
|
|
51
|
+
stdout: "pipe",
|
|
52
|
+
stdin: "pipe",
|
|
53
|
+
stderr: "inherit",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Wait for ready event
|
|
57
|
+
const reader = proc.stdout.getReader();
|
|
58
|
+
const { value } = await reader.read();
|
|
59
|
+
const ready = JSON.parse(new TextDecoder().decode(value));
|
|
60
|
+
|
|
61
|
+
if (ready.event !== "ready") {
|
|
62
|
+
throw new Error("Session failed to start");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const info: SessionInfo = {
|
|
66
|
+
id,
|
|
67
|
+
project,
|
|
68
|
+
workspace,
|
|
69
|
+
socketPath,
|
|
70
|
+
pid: ready.pid,
|
|
71
|
+
attached: false,
|
|
72
|
+
createdAt: Date.now(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const sessionData = { info, proc, stdin: proc.stdin };
|
|
76
|
+
sessions.set(id, sessionData);
|
|
77
|
+
|
|
78
|
+
// Monitor session stdout for state updates
|
|
79
|
+
(async () => {
|
|
80
|
+
while (true) {
|
|
81
|
+
const { done, value } = await reader.read();
|
|
82
|
+
if (done) break;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const lines = new TextDecoder().decode(value).trim().split('\n');
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const event = JSON.parse(line);
|
|
88
|
+
if (event.attached !== undefined) {
|
|
89
|
+
const session = sessions.get(id);
|
|
90
|
+
if (session) session.info.attached = event.attached;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Session ended
|
|
97
|
+
sessions.delete(id);
|
|
98
|
+
console.log(`[router] Session ${id} ended`);
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
console.log(`[router] Created session ${id} for ${project}/${workspace}`);
|
|
102
|
+
return info;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Kick client from session
|
|
106
|
+
async function kickSession(sessionId: string): Promise<boolean> {
|
|
107
|
+
const session = sessions.get(sessionId);
|
|
108
|
+
if (!session) return false;
|
|
109
|
+
|
|
110
|
+
const writer = session.stdin.getWriter();
|
|
111
|
+
await writer.write(new TextEncoder().encode("kick\n"));
|
|
112
|
+
writer.releaseLock();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Start router server
|
|
117
|
+
const server = Bun.listen({
|
|
118
|
+
unix: ROUTER_SOCKET,
|
|
119
|
+
socket: {
|
|
120
|
+
async data(socket, data) {
|
|
121
|
+
try {
|
|
122
|
+
const cmd: RouterCommand = JSON.parse(data.toString());
|
|
123
|
+
let response: RouterResponse;
|
|
124
|
+
|
|
125
|
+
switch (cmd.type) {
|
|
126
|
+
case "list":
|
|
127
|
+
response = {
|
|
128
|
+
type: "sessions",
|
|
129
|
+
sessions: Array.from(sessions.values()).map(s => s.info)
|
|
130
|
+
};
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case "create": {
|
|
134
|
+
// Check if session already exists for this workspace
|
|
135
|
+
const existing = findSession(cmd.project, cmd.workspace);
|
|
136
|
+
|
|
137
|
+
if (existing) {
|
|
138
|
+
if (existing.info.attached) {
|
|
139
|
+
// Session exists and has a client
|
|
140
|
+
response = { type: "already-attached", session: existing.info };
|
|
141
|
+
} else {
|
|
142
|
+
// Session exists but detached - reuse it
|
|
143
|
+
response = { type: "created", session: existing.info };
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Create new session
|
|
147
|
+
const session = await createSession(cmd.project, cmd.workspace, cmd.cwd);
|
|
148
|
+
response = { type: "created", session };
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case "attach": {
|
|
154
|
+
const session = sessions.get(cmd.sessionId);
|
|
155
|
+
if (!session) {
|
|
156
|
+
response = { type: "error", message: "Session not found" };
|
|
157
|
+
} else if (session.info.attached && cmd.mode !== "take-over") {
|
|
158
|
+
response = { type: "already-attached", session: session.info };
|
|
159
|
+
} else {
|
|
160
|
+
if (cmd.mode === "take-over" && session.info.attached) {
|
|
161
|
+
await kickSession(cmd.sessionId);
|
|
162
|
+
await Bun.sleep(50); // Let kick propagate
|
|
163
|
+
}
|
|
164
|
+
response = { type: "created", session: session.info };
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case "kick": {
|
|
170
|
+
const kicked = await kickSession(cmd.sessionId);
|
|
171
|
+
response = kicked ? { type: "ok" } : { type: "error", message: "Session not found" };
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "kill": {
|
|
176
|
+
const toKill = sessions.get(cmd.sessionId);
|
|
177
|
+
if (toKill) {
|
|
178
|
+
toKill.proc.kill();
|
|
179
|
+
sessions.delete(cmd.sessionId);
|
|
180
|
+
response = { type: "ok" };
|
|
181
|
+
} else {
|
|
182
|
+
response = { type: "error", message: "Session not found" };
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
default:
|
|
188
|
+
response = { type: "error", message: "Unknown command" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
socket.write(JSON.stringify(response));
|
|
192
|
+
} catch (e: any) {
|
|
193
|
+
socket.write(JSON.stringify({ type: "error", message: e.message }));
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
error(socket, error) {
|
|
197
|
+
console.error("[router] Client error:", error.message);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
console.log(`[router] Listening on ${ROUTER_SOCKET}`);
|
|
203
|
+
console.log("[router] Ready");
|
|
204
|
+
|
|
205
|
+
// Cleanup on exit
|
|
206
|
+
process.on("SIGTERM", () => {
|
|
207
|
+
for (const [id, session] of sessions) {
|
|
208
|
+
session.proc.kill();
|
|
209
|
+
}
|
|
210
|
+
server.stop();
|
|
211
|
+
try { unlinkSync(ROUTER_SOCKET); } catch {}
|
|
212
|
+
process.exit(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
process.on("SIGINT", () => {
|
|
216
|
+
process.emit("SIGTERM" as any);
|
|
217
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Session server - manages a single PTY session
|
|
4
|
+
* One client at a time, supports kick for takeover
|
|
5
|
+
*
|
|
6
|
+
* Usage: bun session.ts <socket-path> <cwd> <project> <workspace>
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { unlinkSync } from "fs";
|
|
10
|
+
import {
|
|
11
|
+
encodeControl,
|
|
12
|
+
isControl,
|
|
13
|
+
decodeControl,
|
|
14
|
+
type SessionControl,
|
|
15
|
+
} from "./protocol";
|
|
16
|
+
|
|
17
|
+
const [socketPath, cwd, project, workspace] = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
if (!socketPath || !cwd) {
|
|
20
|
+
console.error("Usage: bun session.ts <socket-path> <cwd> <project> <workspace>");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Clean up existing socket
|
|
25
|
+
try { unlinkSync(socketPath); } catch {}
|
|
26
|
+
|
|
27
|
+
// Scrollback buffer (keep last 50KB)
|
|
28
|
+
const MAX_SCROLLBACK = 50 * 1024;
|
|
29
|
+
let scrollback = Buffer.alloc(0);
|
|
30
|
+
|
|
31
|
+
// Current attached client (only one allowed)
|
|
32
|
+
let client: any = null;
|
|
33
|
+
|
|
34
|
+
// Create PTY
|
|
35
|
+
const terminal = new Bun.Terminal({
|
|
36
|
+
cols: 120,
|
|
37
|
+
rows: 40,
|
|
38
|
+
data(term, data) {
|
|
39
|
+
// Add to scrollback
|
|
40
|
+
scrollback = Buffer.concat([scrollback, data]);
|
|
41
|
+
if (scrollback.length > MAX_SCROLLBACK) {
|
|
42
|
+
scrollback = scrollback.subarray(-MAX_SCROLLBACK);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Send to attached client
|
|
46
|
+
if (client) {
|
|
47
|
+
client.write(data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Spawn shell
|
|
53
|
+
const proc = Bun.spawn(["bash"], {
|
|
54
|
+
terminal,
|
|
55
|
+
cwd,
|
|
56
|
+
env: {
|
|
57
|
+
...process.env,
|
|
58
|
+
SPACES_PROJECT: project,
|
|
59
|
+
SPACES_WORKSPACE: workspace,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Handle shell exit
|
|
64
|
+
proc.exited.then(code => {
|
|
65
|
+
if (client) {
|
|
66
|
+
client.write(encodeControl({ type: "exited", code }));
|
|
67
|
+
}
|
|
68
|
+
// Give client time to receive exit message
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
server.stop();
|
|
71
|
+
try { unlinkSync(socketPath); } catch {}
|
|
72
|
+
process.exit(code);
|
|
73
|
+
}, 100);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Report state to router
|
|
77
|
+
function reportState(event: string) {
|
|
78
|
+
console.log(JSON.stringify({ event, attached: client !== null }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Kick current client
|
|
82
|
+
function kickClient() {
|
|
83
|
+
if (client) {
|
|
84
|
+
client.write(encodeControl({ type: "kicked" }));
|
|
85
|
+
client.end();
|
|
86
|
+
client = null;
|
|
87
|
+
reportState("client_kicked");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Start server
|
|
92
|
+
const server = Bun.listen({
|
|
93
|
+
unix: socketPath,
|
|
94
|
+
socket: {
|
|
95
|
+
open(socket) {
|
|
96
|
+
if (client) {
|
|
97
|
+
// Already have a client - kick them
|
|
98
|
+
kickClient();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
client = socket;
|
|
102
|
+
reportState("client_attached");
|
|
103
|
+
|
|
104
|
+
// Send scrollback to new client
|
|
105
|
+
const attachMsg = encodeControl({
|
|
106
|
+
type: "attached",
|
|
107
|
+
scrollback: scrollback.toString("base64")
|
|
108
|
+
});
|
|
109
|
+
socket.write(attachMsg);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
data(socket, data) {
|
|
113
|
+
const buf = Buffer.from(data);
|
|
114
|
+
|
|
115
|
+
if (isControl(buf)) {
|
|
116
|
+
const ctrl = decodeControl(buf) as SessionControl;
|
|
117
|
+
|
|
118
|
+
switch (ctrl.type) {
|
|
119
|
+
case "resize":
|
|
120
|
+
terminal.resize(ctrl.cols, ctrl.rows);
|
|
121
|
+
break;
|
|
122
|
+
case "detach":
|
|
123
|
+
if (socket === client) {
|
|
124
|
+
client = null;
|
|
125
|
+
reportState("client_detached");
|
|
126
|
+
}
|
|
127
|
+
socket.end();
|
|
128
|
+
break;
|
|
129
|
+
case "ping":
|
|
130
|
+
socket.write(encodeControl({ type: "pong" }));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
// Raw input - write to PTY
|
|
135
|
+
terminal.write(buf);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
close(socket) {
|
|
140
|
+
if (socket === client) {
|
|
141
|
+
client = null;
|
|
142
|
+
reportState("client_disconnected");
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
error(socket, error) {
|
|
147
|
+
console.error(JSON.stringify({ event: "error", message: error.message }));
|
|
148
|
+
if (socket === client) {
|
|
149
|
+
client = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
console.log(JSON.stringify({
|
|
156
|
+
event: "ready",
|
|
157
|
+
socketPath,
|
|
158
|
+
pid: process.pid,
|
|
159
|
+
project,
|
|
160
|
+
workspace,
|
|
161
|
+
attached: false
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
// Handle stdin commands from router (for kick)
|
|
165
|
+
const decoder = new TextDecoder();
|
|
166
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
167
|
+
const cmd = decoder.decode(chunk).trim();
|
|
168
|
+
if (cmd === "kick") {
|
|
169
|
+
kickClient();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle termination
|
|
174
|
+
process.on("SIGTERM", () => {
|
|
175
|
+
proc.kill();
|
|
176
|
+
terminal.close();
|
|
177
|
+
server.stop();
|
|
178
|
+
try { unlinkSync(socketPath); } catch {}
|
|
179
|
+
process.exit(0);
|
|
180
|
+
});
|