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,796 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* tmux-lite CLI and API
|
|
4
|
+
*
|
|
5
|
+
* CLI Commands:
|
|
6
|
+
* tl new [name] Create new session
|
|
7
|
+
* tl a|attach [id] Attach to session
|
|
8
|
+
* tl ls|list List sessions
|
|
9
|
+
* tl kill <id> Kill a session
|
|
10
|
+
* tl kill-server Stop the server
|
|
11
|
+
*
|
|
12
|
+
* API: Import and use the exported functions
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "bun";
|
|
16
|
+
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
17
|
+
import { select } from "@inquirer/prompts";
|
|
18
|
+
import { createBufferedSocketWriter } from "../../utils/bun-socket-writer";
|
|
19
|
+
import {
|
|
20
|
+
getRouterSocket,
|
|
21
|
+
getPidFile,
|
|
22
|
+
PROTOCOL_VERSION,
|
|
23
|
+
PACKAGE_VERSION,
|
|
24
|
+
type Command,
|
|
25
|
+
type Response,
|
|
26
|
+
type Session,
|
|
27
|
+
type SessionEvent,
|
|
28
|
+
type InboxItem,
|
|
29
|
+
encodeRouterMessage,
|
|
30
|
+
decodeRouterMessages,
|
|
31
|
+
encodeControl,
|
|
32
|
+
encodePTY,
|
|
33
|
+
parseFrames,
|
|
34
|
+
decodeControl,
|
|
35
|
+
FrameType,
|
|
36
|
+
} from "./protocol";
|
|
37
|
+
|
|
38
|
+
// Re-export types
|
|
39
|
+
export type { Session, InboxItem, Command, Response };
|
|
40
|
+
|
|
41
|
+
// Re-export constants
|
|
42
|
+
export { PROTOCOL_VERSION, PACKAGE_VERSION, getRouterSocket, getPidFile };
|
|
43
|
+
|
|
44
|
+
/** Status response from server */
|
|
45
|
+
export interface ServerStatus {
|
|
46
|
+
version: string;
|
|
47
|
+
protocol: number;
|
|
48
|
+
pid: number;
|
|
49
|
+
uptime: number;
|
|
50
|
+
sessions: number;
|
|
51
|
+
attached: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Terminal reset - RIS (Reset to Initial State) resets everything
|
|
55
|
+
const TERM_RESET = "\x1bc";
|
|
56
|
+
|
|
57
|
+
const SERVER_SCRIPT = `${import.meta.dir}/server.ts`;
|
|
58
|
+
|
|
59
|
+
// CLI args
|
|
60
|
+
const rawArgs = process.argv.slice(2);
|
|
61
|
+
const isTestMode = rawArgs.includes("--test");
|
|
62
|
+
const args = rawArgs.filter(arg => arg !== "--test");
|
|
63
|
+
const cmd = args[0] || "list";
|
|
64
|
+
|
|
65
|
+
if (isTestMode) {
|
|
66
|
+
process.env.TMUX_LITE_SOCKET = "/tmp/tmux-lite-test.sock";
|
|
67
|
+
process.env.TMUX_LITE_SESSION_DIR = "/tmp/tmux-lite-test";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const getServerCommand = (): string[] => (
|
|
71
|
+
isTestMode ? ["bun", "run", SERVER_SCRIPT, "--test"] : ["bun", "run", SERVER_SCRIPT]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Check if we're already inside a tmux-lite session
|
|
75
|
+
export function isNested(): boolean {
|
|
76
|
+
return !!process.env.TMUX_LITE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function checkNested(): boolean {
|
|
80
|
+
if (isNested()) {
|
|
81
|
+
console.error("Error: Already inside tmux-lite session " + process.env.TMUX_LITE);
|
|
82
|
+
console.error("Nested sessions are not supported. Detach first with Ctrl+Esc.");
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if server is running
|
|
89
|
+
export async function isServerRunning(): Promise<boolean> {
|
|
90
|
+
const routerSocket = getRouterSocket();
|
|
91
|
+
if (!existsSync(routerSocket)) return false;
|
|
92
|
+
try {
|
|
93
|
+
await send({ type: "list" });
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Start server if not running
|
|
101
|
+
export async function ensureServer(): Promise<void> {
|
|
102
|
+
if (await isServerRunning()) return;
|
|
103
|
+
|
|
104
|
+
spawn({
|
|
105
|
+
cmd: getServerCommand(),
|
|
106
|
+
stdout: "ignore",
|
|
107
|
+
stderr: "ignore",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < 30; i++) {
|
|
111
|
+
await Bun.sleep(100);
|
|
112
|
+
if (await isServerRunning()) return;
|
|
113
|
+
}
|
|
114
|
+
throw new Error("Failed to start tmux-lite server");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a process with given PID is running
|
|
119
|
+
*/
|
|
120
|
+
export function isProcessRunning(pid: number): boolean {
|
|
121
|
+
try {
|
|
122
|
+
// Signal 0 doesn't kill - just checks if process exists
|
|
123
|
+
process.kill(pid, 0);
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get server PID from PID file
|
|
132
|
+
* Returns null if PID file doesn't exist or is invalid
|
|
133
|
+
*/
|
|
134
|
+
export function getServerPid(): number | null {
|
|
135
|
+
const pidFile = getPidFile();
|
|
136
|
+
if (!existsSync(pidFile)) return null;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const content = readFileSync(pidFile, "utf-8").trim();
|
|
140
|
+
const pid = parseInt(content, 10);
|
|
141
|
+
if (isNaN(pid) || pid <= 0) return null;
|
|
142
|
+
return pid;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Clean up stale PID file if process is not running
|
|
150
|
+
* Returns true if cleanup was needed
|
|
151
|
+
*/
|
|
152
|
+
export function cleanupStalePidFile(): boolean {
|
|
153
|
+
const pid = getServerPid();
|
|
154
|
+
if (pid === null) return false;
|
|
155
|
+
|
|
156
|
+
if (!isProcessRunning(pid)) {
|
|
157
|
+
// Process is dead, clean up stale files
|
|
158
|
+
const pidFile = getPidFile();
|
|
159
|
+
const routerSocket = getRouterSocket();
|
|
160
|
+
try { unlinkSync(pidFile); } catch {}
|
|
161
|
+
try { unlinkSync(routerSocket); } catch {}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get server version info
|
|
169
|
+
*/
|
|
170
|
+
export async function getVersion(): Promise<{ version: string; protocol: number }> {
|
|
171
|
+
await ensureServer();
|
|
172
|
+
const res = await send({ type: "version" });
|
|
173
|
+
if (res.type === "version") {
|
|
174
|
+
return { version: res.version, protocol: res.protocol };
|
|
175
|
+
}
|
|
176
|
+
throw new Error("Unexpected response");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get server status (version + stats)
|
|
181
|
+
*/
|
|
182
|
+
export async function getStatus(): Promise<ServerStatus> {
|
|
183
|
+
await ensureServer();
|
|
184
|
+
const res = await send({ type: "status" });
|
|
185
|
+
if (res.type === "status") {
|
|
186
|
+
return {
|
|
187
|
+
version: res.version,
|
|
188
|
+
protocol: res.protocol,
|
|
189
|
+
pid: res.pid,
|
|
190
|
+
uptime: res.uptime,
|
|
191
|
+
sessions: res.sessions,
|
|
192
|
+
attached: res.attached,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
throw new Error("Unexpected response");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Alias for killServer - stops the server daemon
|
|
200
|
+
*/
|
|
201
|
+
export const stopServer = killServer;
|
|
202
|
+
|
|
203
|
+
// Send command to server
|
|
204
|
+
export async function send(cmd: Command): Promise<Response> {
|
|
205
|
+
return new Promise(async (resolve, reject) => {
|
|
206
|
+
let buffer: Buffer = Buffer.alloc(0);
|
|
207
|
+
let settled = false;
|
|
208
|
+
let socketRef: Awaited<ReturnType<typeof Bun.connect>> | null = null;
|
|
209
|
+
let socketWriter: ReturnType<typeof createBufferedSocketWriter> | null = null;
|
|
210
|
+
|
|
211
|
+
const fail = (err: Error) => {
|
|
212
|
+
if (settled) return;
|
|
213
|
+
settled = true;
|
|
214
|
+
socketRef?.end();
|
|
215
|
+
reject(err);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const routerSocket = getRouterSocket();
|
|
220
|
+
const socket = await Bun.connect({
|
|
221
|
+
unix: routerSocket,
|
|
222
|
+
socket: {
|
|
223
|
+
drain() {
|
|
224
|
+
socketWriter?.flush();
|
|
225
|
+
},
|
|
226
|
+
data(socket, data) {
|
|
227
|
+
if (settled) return;
|
|
228
|
+
buffer = Buffer.concat([buffer, Buffer.from(data)]);
|
|
229
|
+
let decoded;
|
|
230
|
+
try {
|
|
231
|
+
decoded = decodeRouterMessages(buffer);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
fail(err instanceof Error ? err : new Error("Invalid response"));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
buffer = decoded.remaining as Buffer;
|
|
237
|
+
if (decoded.messages.length > 0) {
|
|
238
|
+
settled = true;
|
|
239
|
+
resolve(decoded.messages[0] as Response);
|
|
240
|
+
socket.end();
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
close() {
|
|
244
|
+
if (!settled) {
|
|
245
|
+
fail(new Error("Connection closed before response"));
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
error(_, e) { fail(e); },
|
|
249
|
+
connectError(_, e) { fail(e); }
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
socketRef = socket;
|
|
253
|
+
socketWriter = createBufferedSocketWriter(socket);
|
|
254
|
+
socketWriter.write(encodeRouterMessage(cmd));
|
|
255
|
+
} catch (e) {
|
|
256
|
+
fail(e instanceof Error ? e : new Error(String(e)));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// === API convenience functions ===
|
|
262
|
+
|
|
263
|
+
export async function listSessions(): Promise<Session[]> {
|
|
264
|
+
await ensureServer();
|
|
265
|
+
const res = await send({ type: "list" });
|
|
266
|
+
if (res.type === "sessions") return res.sessions;
|
|
267
|
+
throw new Error("Unexpected response");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function createSession(name: string, cwd: string): Promise<Session> {
|
|
271
|
+
await ensureServer();
|
|
272
|
+
const res = await send({ type: "new", name, cwd });
|
|
273
|
+
if (res.type === "session") return res.session;
|
|
274
|
+
if (res.type === "error") throw new Error(res.message);
|
|
275
|
+
throw new Error("Unexpected response");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function killSession(id: string): Promise<void> {
|
|
279
|
+
await ensureServer();
|
|
280
|
+
const res = await send({ type: "kill", id });
|
|
281
|
+
if (res.type === "error") throw new Error(res.message);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function killServer(): Promise<void> {
|
|
285
|
+
if (!(await isServerRunning())) return;
|
|
286
|
+
await send({ type: "kill-server" });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function getInbox(): Promise<InboxItem[]> {
|
|
290
|
+
await ensureServer();
|
|
291
|
+
const res = await send({ type: "inbox" });
|
|
292
|
+
if (res.type === "inbox") return res.items;
|
|
293
|
+
throw new Error("Unexpected response");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function getUnreadCount(): Promise<number> {
|
|
297
|
+
const items = await getInbox();
|
|
298
|
+
return items.filter(i => !i.read).length;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function clearInbox(id?: string): Promise<void> {
|
|
302
|
+
await ensureServer();
|
|
303
|
+
await send({ type: "inbox-clear", id });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function markInboxRead(id: string): Promise<void> {
|
|
307
|
+
await ensureServer();
|
|
308
|
+
await send({ type: "inbox-read", id });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Format session for display
|
|
312
|
+
function formatSession(s: Session): string {
|
|
313
|
+
const age = Math.floor((Date.now() - s.createdAt) / 1000);
|
|
314
|
+
const ageStr = age < 60 ? `${age}s` : age < 3600 ? `${Math.floor(age/60)}m` : `${Math.floor(age/3600)}h`;
|
|
315
|
+
const status = s.attached ? "\x1b[32m●\x1b[0m" : "\x1b[90m○\x1b[0m";
|
|
316
|
+
const title = s.processTitle ? ` \x1b[33m[${s.processTitle}]\x1b[0m` : "";
|
|
317
|
+
return `${status} ${s.id}: ${s.name} (${ageStr})${title} ${s.cwd}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Ctrl+Esc sequences (different terminals send different formats)
|
|
321
|
+
const CTRL_ESC_CSI_U = Buffer.from([0x1b, 0x5b, 0x32, 0x37, 0x3b, 0x35, 0x75]); // ESC [ 27;5u
|
|
322
|
+
const CTRL_ESC_XTERM = Buffer.from([0x1b, 0x5b, 0x32, 0x37, 0x3b, 0x35, 0x3b, 0x32, 0x37, 0x7e]); // ESC [ 27;5;27 ~
|
|
323
|
+
const BRACKETED_PASTE_START = Buffer.from([0x1b, 0x5b, 0x32, 0x30, 0x30, 0x7e]); // ESC [ 200 ~
|
|
324
|
+
const BRACKETED_PASTE_END = Buffer.from([0x1b, 0x5b, 0x32, 0x30, 0x31, 0x7e]); // ESC [ 201 ~
|
|
325
|
+
|
|
326
|
+
function containsCtrlEsc(buf: Buffer): number {
|
|
327
|
+
const idx1 = buf.indexOf(CTRL_ESC_CSI_U);
|
|
328
|
+
const idx2 = buf.indexOf(CTRL_ESC_XTERM);
|
|
329
|
+
if (idx1 === -1) return idx2;
|
|
330
|
+
if (idx2 === -1) return idx1;
|
|
331
|
+
return Math.min(idx1, idx2);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export type AttachResult =
|
|
335
|
+
| { type: "detached" }
|
|
336
|
+
| { type: "exited"; code: number }
|
|
337
|
+
| { type: "kicked" }
|
|
338
|
+
| { type: "error"; message: string };
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Attach to a session interactively.
|
|
342
|
+
* Takes over stdin/stdout. Returns when session ends or user detaches.
|
|
343
|
+
* @param session Session to attach to
|
|
344
|
+
* @param quiet If true, don't print attach/detach messages
|
|
345
|
+
*/
|
|
346
|
+
export async function attach(session: Session, quiet: boolean = false): Promise<AttachResult> {
|
|
347
|
+
if (!quiet) {
|
|
348
|
+
console.log(`Attaching to ${session.name}...`);
|
|
349
|
+
console.log("Ctrl+Esc to detach\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return new Promise(async (resolve) => {
|
|
353
|
+
let buffer = Buffer.alloc(0);
|
|
354
|
+
let pendingSeq = Buffer.alloc(0);
|
|
355
|
+
let inBracketedPaste = false;
|
|
356
|
+
let resolved = false;
|
|
357
|
+
let stdinListener: ((chunk: Buffer) => void) | null = null;
|
|
358
|
+
let socket: Awaited<ReturnType<typeof Bun.connect>> | null = null;
|
|
359
|
+
let socketWriter: ReturnType<typeof createBufferedSocketWriter> | null = null;
|
|
360
|
+
let onResize: (() => void) | null = null;
|
|
361
|
+
let lastSize = { cols: 0, rows: 0 };
|
|
362
|
+
|
|
363
|
+
const cleanup = (result: AttachResult) => {
|
|
364
|
+
if (resolved) return;
|
|
365
|
+
resolved = true;
|
|
366
|
+
if (stdinListener) {
|
|
367
|
+
process.stdin.removeListener("data", stdinListener);
|
|
368
|
+
}
|
|
369
|
+
process.stdin.setRawMode(false);
|
|
370
|
+
process.stdin.pause();
|
|
371
|
+
process.stdout.write(TERM_RESET);
|
|
372
|
+
if (onResize) {
|
|
373
|
+
process.removeListener("SIGWINCH", onResize);
|
|
374
|
+
}
|
|
375
|
+
socket = null;
|
|
376
|
+
socketWriter = null;
|
|
377
|
+
if (!quiet) {
|
|
378
|
+
if (result.type === "detached") console.log("\n[detached]");
|
|
379
|
+
else if (result.type === "exited") console.log(`\n[exited: ${result.code}]`);
|
|
380
|
+
else if (result.type === "kicked") console.log("\n[kicked - another client took over]");
|
|
381
|
+
else if (result.type === "error") console.error("\n[error]", result.message);
|
|
382
|
+
}
|
|
383
|
+
resolve(result);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const getTermSize = () => {
|
|
387
|
+
let cols = process.stdout.columns || 0;
|
|
388
|
+
let rows = process.stdout.rows || 0;
|
|
389
|
+
if (cols <= 0 || rows <= 0) {
|
|
390
|
+
const size = (process.stdout as { getWindowSize?: () => number[] }).getWindowSize?.();
|
|
391
|
+
if (Array.isArray(size) && size.length >= 2) {
|
|
392
|
+
cols = size[0];
|
|
393
|
+
rows = size[1];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
cols: cols > 0 ? cols : 80,
|
|
398
|
+
rows: rows > 0 ? rows : 24,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const sendResize = (force = false) => {
|
|
403
|
+
if (!socket) return;
|
|
404
|
+
const { cols, rows } = getTermSize();
|
|
405
|
+
if (!force && cols === lastSize.cols && rows === lastSize.rows) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
lastSize = { cols, rows };
|
|
409
|
+
const frame = encodeControl({ type: "resize", cols, rows });
|
|
410
|
+
if (socketWriter) socketWriter.write(frame);
|
|
411
|
+
else socket.write(frame);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const sendAttachInit = () => {
|
|
415
|
+
if (!socket) return;
|
|
416
|
+
const { cols, rows } = getTermSize();
|
|
417
|
+
const frame = encodeControl({ type: "attach-init", cols, rows, clientType: "cli" });
|
|
418
|
+
if (socketWriter) socketWriter.write(frame);
|
|
419
|
+
else socket.write(frame);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
socket = await Bun.connect({
|
|
423
|
+
unix: session.socketPath,
|
|
424
|
+
socket: {
|
|
425
|
+
drain() {
|
|
426
|
+
socketWriter?.flush();
|
|
427
|
+
},
|
|
428
|
+
data(socket, data) {
|
|
429
|
+
let buf = Buffer.from(data);
|
|
430
|
+
|
|
431
|
+
if (buffer.length > 0) {
|
|
432
|
+
buf = Buffer.concat([buffer, buf]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Parse frames from the buffer
|
|
436
|
+
let frames;
|
|
437
|
+
let remaining;
|
|
438
|
+
try {
|
|
439
|
+
const result = parseFrames(buf);
|
|
440
|
+
frames = result.frames;
|
|
441
|
+
remaining = result.remaining;
|
|
442
|
+
} catch (err) {
|
|
443
|
+
// Protocol error - likely desync or corrupted data
|
|
444
|
+
const msg = err instanceof Error ? err.message : 'Frame parse error';
|
|
445
|
+
console.error(`[attach] Frame parse error: ${msg}`);
|
|
446
|
+
cleanup({ type: "error", message: msg });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
buffer = Buffer.from(remaining);
|
|
450
|
+
|
|
451
|
+
for (const frame of frames) {
|
|
452
|
+
if (frame.type === FrameType.CONTROL) {
|
|
453
|
+
const event = decodeControl(frame.payload) as SessionEvent;
|
|
454
|
+
|
|
455
|
+
if (event.type === "attached") {
|
|
456
|
+
// Send a single resize to ensure proper dimensions
|
|
457
|
+
sendResize(true);
|
|
458
|
+
} else if (event.type === "exited") {
|
|
459
|
+
cleanup({ type: "exited", code: event.code });
|
|
460
|
+
return;
|
|
461
|
+
} else if (event.type === "kicked") {
|
|
462
|
+
cleanup({ type: "kicked" });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
} else if (frame.type === FrameType.PTY) {
|
|
466
|
+
process.stdout.write(frame.payload);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
close() {
|
|
472
|
+
cleanup({ type: "detached" });
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
error(_, e) {
|
|
476
|
+
cleanup({ type: "error", message: e.message });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
socketWriter = createBufferedSocketWriter(socket);
|
|
481
|
+
|
|
482
|
+
// Initial resize
|
|
483
|
+
sendAttachInit();
|
|
484
|
+
sendResize(true);
|
|
485
|
+
|
|
486
|
+
onResize = () => {
|
|
487
|
+
sendResize();
|
|
488
|
+
};
|
|
489
|
+
process.on("SIGWINCH", onResize);
|
|
490
|
+
process.stdin.setRawMode(true);
|
|
491
|
+
process.stdin.resume();
|
|
492
|
+
|
|
493
|
+
// Forward stdin with Ctrl+Esc detection
|
|
494
|
+
stdinListener = (chunk: Buffer) => {
|
|
495
|
+
const combined = pendingSeq.length > 0 ? Buffer.concat([pendingSeq, chunk]) : chunk;
|
|
496
|
+
pendingSeq = Buffer.alloc(0);
|
|
497
|
+
const out: Buffer[] = [];
|
|
498
|
+
let offset = 0;
|
|
499
|
+
|
|
500
|
+
const flushOut = () => {
|
|
501
|
+
if (out.length > 0 && socket) {
|
|
502
|
+
const frame = encodePTY(Buffer.concat(out));
|
|
503
|
+
if (socketWriter) socketWriter.write(frame);
|
|
504
|
+
else socket.write(frame);
|
|
505
|
+
out.length = 0;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const getSequences = () => (
|
|
510
|
+
inBracketedPaste
|
|
511
|
+
? [BRACKETED_PASTE_START, BRACKETED_PASTE_END]
|
|
512
|
+
: [BRACKETED_PASTE_START, BRACKETED_PASTE_END, CTRL_ESC_CSI_U, CTRL_ESC_XTERM]
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
while (offset < combined.length) {
|
|
516
|
+
if (combined[offset] !== 0x1b) {
|
|
517
|
+
const nextEsc = combined.indexOf(0x1b, offset + 1);
|
|
518
|
+
if (nextEsc === -1) {
|
|
519
|
+
out.push(combined.subarray(offset));
|
|
520
|
+
offset = combined.length;
|
|
521
|
+
} else {
|
|
522
|
+
out.push(combined.subarray(offset, nextEsc));
|
|
523
|
+
offset = nextEsc;
|
|
524
|
+
}
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const sequences = getSequences();
|
|
529
|
+
let matched: Buffer | null = null;
|
|
530
|
+
for (const seq of sequences) {
|
|
531
|
+
if (combined.length - offset >= seq.length &&
|
|
532
|
+
combined.subarray(offset, offset + seq.length).equals(seq)) {
|
|
533
|
+
matched = seq;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (matched) {
|
|
539
|
+
if (matched === CTRL_ESC_CSI_U || matched === CTRL_ESC_XTERM) {
|
|
540
|
+
flushOut();
|
|
541
|
+
if (socket) {
|
|
542
|
+
const frame = encodeControl({ type: "detach" });
|
|
543
|
+
if (socketWriter) socketWriter.write(frame);
|
|
544
|
+
else socket.write(frame);
|
|
545
|
+
}
|
|
546
|
+
cleanup({ type: "detached" });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
out.push(combined.subarray(offset, offset + matched.length));
|
|
551
|
+
if (matched === BRACKETED_PASTE_START) {
|
|
552
|
+
inBracketedPaste = true;
|
|
553
|
+
} else if (matched === BRACKETED_PASTE_END) {
|
|
554
|
+
inBracketedPaste = false;
|
|
555
|
+
}
|
|
556
|
+
offset += matched.length;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
let possiblePrefix = false;
|
|
561
|
+
for (const seq of sequences) {
|
|
562
|
+
const remaining = combined.length - offset;
|
|
563
|
+
if (remaining < seq.length &&
|
|
564
|
+
seq.subarray(0, remaining).equals(combined.subarray(offset))) {
|
|
565
|
+
possiblePrefix = true;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (possiblePrefix) {
|
|
571
|
+
pendingSeq = Buffer.from(combined.subarray(offset));
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
out.push(combined.subarray(offset, offset + 1));
|
|
576
|
+
offset += 1;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
flushOut();
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
process.stdin.on("data", stdinListener);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Handle attach result and exit with appropriate code
|
|
587
|
+
function handleAttachResult(result: AttachResult): void {
|
|
588
|
+
if (result.type === "exited") {
|
|
589
|
+
process.exit(result.code);
|
|
590
|
+
} else if (result.type === "error") {
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
// detached and kicked exit cleanly
|
|
594
|
+
process.exit(0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Main
|
|
598
|
+
async function main() {
|
|
599
|
+
// Start server if not running
|
|
600
|
+
if (!(await isServerRunning())) {
|
|
601
|
+
if (cmd === "kill-server") {
|
|
602
|
+
console.log("Server not running");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
console.log("Starting server...");
|
|
606
|
+
spawn({
|
|
607
|
+
cmd: getServerCommand(),
|
|
608
|
+
stdout: "inherit",
|
|
609
|
+
stderr: "inherit",
|
|
610
|
+
});
|
|
611
|
+
await Bun.sleep(300);
|
|
612
|
+
if (!(await isServerRunning())) {
|
|
613
|
+
console.error("Failed to start server");
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
switch (cmd) {
|
|
619
|
+
case "new": {
|
|
620
|
+
if (checkNested()) process.exit(1);
|
|
621
|
+
const name = args[1];
|
|
622
|
+
const res = await send({ type: "new", name, cwd: process.cwd() });
|
|
623
|
+
if (res.type === "session") {
|
|
624
|
+
const result = await attach(res.session);
|
|
625
|
+
handleAttachResult(result);
|
|
626
|
+
} else if (res.type === "error") {
|
|
627
|
+
console.error("Error:", res.message);
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
case "a":
|
|
633
|
+
case "attach": {
|
|
634
|
+
if (checkNested()) process.exit(1);
|
|
635
|
+
const id = args[1];
|
|
636
|
+
if (id) {
|
|
637
|
+
const res = await send({ type: "attach", id, force: args.includes("-f") });
|
|
638
|
+
if (res.type === "session") {
|
|
639
|
+
const result = await attach(res.session);
|
|
640
|
+
handleAttachResult(result);
|
|
641
|
+
} else if (res.type === "already-attached") {
|
|
642
|
+
console.log(`Session ${id} is attached elsewhere.\n`);
|
|
643
|
+
const choice = await select({
|
|
644
|
+
message: "What to do?",
|
|
645
|
+
choices: [
|
|
646
|
+
{ value: "force", name: "Take over" },
|
|
647
|
+
{ value: "cancel", name: "Cancel" },
|
|
648
|
+
]
|
|
649
|
+
});
|
|
650
|
+
if (choice === "force") {
|
|
651
|
+
const res2 = await send({ type: "attach", id, force: true });
|
|
652
|
+
if (res2.type === "session") {
|
|
653
|
+
const result = await attach(res2.session);
|
|
654
|
+
handleAttachResult(result);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} else if (res.type === "error") {
|
|
658
|
+
console.error("Error:", res.message);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
// No ID - show picker
|
|
662
|
+
const res = await send({ type: "list" });
|
|
663
|
+
if (res.type === "sessions") {
|
|
664
|
+
if (res.sessions.length === 0) {
|
|
665
|
+
console.log("No sessions. Create with: tl new");
|
|
666
|
+
} else {
|
|
667
|
+
const choice = await select({
|
|
668
|
+
message: "Select session:",
|
|
669
|
+
choices: res.sessions.map(s => ({
|
|
670
|
+
value: s.id,
|
|
671
|
+
name: formatSession(s)
|
|
672
|
+
}))
|
|
673
|
+
});
|
|
674
|
+
const res2 = await send({ type: "attach", id: choice });
|
|
675
|
+
if (res2.type === "session") {
|
|
676
|
+
const result = await attach(res2.session);
|
|
677
|
+
handleAttachResult(result);
|
|
678
|
+
} else if (res2.type === "already-attached") {
|
|
679
|
+
const force = await select({
|
|
680
|
+
message: "Session attached. Take over?",
|
|
681
|
+
choices: [
|
|
682
|
+
{ value: true, name: "Yes" },
|
|
683
|
+
{ value: false, name: "No" },
|
|
684
|
+
]
|
|
685
|
+
});
|
|
686
|
+
if (force) {
|
|
687
|
+
const res3 = await send({ type: "attach", id: choice, force: true });
|
|
688
|
+
if (res3.type === "session") {
|
|
689
|
+
const result = await attach(res3.session);
|
|
690
|
+
handleAttachResult(result);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
case "ls":
|
|
701
|
+
case "list": {
|
|
702
|
+
const res = await send({ type: "list" });
|
|
703
|
+
if (res.type === "sessions") {
|
|
704
|
+
if (res.sessions.length === 0) {
|
|
705
|
+
console.log("No sessions");
|
|
706
|
+
} else {
|
|
707
|
+
console.log("Sessions:");
|
|
708
|
+
for (const s of res.sessions) {
|
|
709
|
+
console.log(" " + formatSession(s));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
case "kill": {
|
|
717
|
+
if (checkNested()) process.exit(1);
|
|
718
|
+
const id = args[1];
|
|
719
|
+
if (!id) {
|
|
720
|
+
console.error("Usage: tl kill <id>");
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
const res = await send({ type: "kill", id });
|
|
724
|
+
if (res.type === "ok") {
|
|
725
|
+
console.log(`Killed ${id}`);
|
|
726
|
+
} else if (res.type === "error") {
|
|
727
|
+
console.error("Error:", res.message);
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
case "kill-server": {
|
|
733
|
+
if (checkNested()) process.exit(1);
|
|
734
|
+
await send({ type: "kill-server" });
|
|
735
|
+
console.log("Server stopped");
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
case "inbox": {
|
|
740
|
+
const res = await send({ type: "inbox" });
|
|
741
|
+
if (res.type === "inbox") {
|
|
742
|
+
if (res.items.length === 0) {
|
|
743
|
+
console.log("Inbox empty");
|
|
744
|
+
} else {
|
|
745
|
+
console.log("Inbox:");
|
|
746
|
+
for (const item of res.items) {
|
|
747
|
+
const icon = item.type === 'exit'
|
|
748
|
+
? (item.exitCode === 0 ? '✓' : '✖')
|
|
749
|
+
: '🔔';
|
|
750
|
+
const status = item.read ? '' : ' (unread)';
|
|
751
|
+
const time = new Date(item.timestamp).toLocaleTimeString();
|
|
752
|
+
console.log(` ${icon} [${time}] ${item.sessionName}${status}`);
|
|
753
|
+
// Indent context lines
|
|
754
|
+
const lines = item.context.split('\n').slice(0, 3);
|
|
755
|
+
for (const line of lines) {
|
|
756
|
+
console.log(` ${line}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
case "inbox-clear": {
|
|
765
|
+
const id = args[1];
|
|
766
|
+
await send({ type: "inbox-clear", id });
|
|
767
|
+
console.log(id ? `Cleared inbox item ${id}` : "Inbox cleared");
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
default:
|
|
772
|
+
console.log(`
|
|
773
|
+
tmux-lite
|
|
774
|
+
|
|
775
|
+
Commands:
|
|
776
|
+
tl new [name] Create session
|
|
777
|
+
tl attach [id] Attach (picker if no id)
|
|
778
|
+
tl list List sessions
|
|
779
|
+
tl kill <id> Kill session
|
|
780
|
+
tl kill-server Stop server
|
|
781
|
+
tl inbox Show inbox (bells, exits)
|
|
782
|
+
tl inbox-clear Clear inbox
|
|
783
|
+
|
|
784
|
+
In session:
|
|
785
|
+
Ctrl+Esc Detach
|
|
786
|
+
`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Only run CLI when executed directly, not when imported as a module
|
|
791
|
+
if (import.meta.main) {
|
|
792
|
+
main().catch(e => {
|
|
793
|
+
console.error(e.message);
|
|
794
|
+
process.exit(1);
|
|
795
|
+
});
|
|
796
|
+
}
|