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,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalMachineProvider
|
|
3
|
+
*
|
|
4
|
+
* MachineProvider implementation for the local machine.
|
|
5
|
+
* Wraps existing TUI state functions and tmux-lite CLI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { hostname } from 'os';
|
|
11
|
+
import { createBufferedSocketWriter } from '../../utils/bun-socket-writer.js';
|
|
12
|
+
import {
|
|
13
|
+
getAllProjectNames,
|
|
14
|
+
readProjectConfig,
|
|
15
|
+
getCurrentProject,
|
|
16
|
+
getProjectWorkspacesDir,
|
|
17
|
+
} from '../../core/config.js';
|
|
18
|
+
import { getWorktreeInfo } from '../../core/git.js';
|
|
19
|
+
import {
|
|
20
|
+
listSessions as tmuxListSessions,
|
|
21
|
+
createSession as tmuxCreateSession,
|
|
22
|
+
getInbox as tmuxGetInbox,
|
|
23
|
+
markInboxRead as tmuxMarkInboxRead,
|
|
24
|
+
clearInbox as tmuxClearInbox,
|
|
25
|
+
ensureServer,
|
|
26
|
+
send,
|
|
27
|
+
type Session as TmuxSession,
|
|
28
|
+
} from '../../lib/tmux-lite/cli.js';
|
|
29
|
+
import type {
|
|
30
|
+
MachineProvider,
|
|
31
|
+
CreateSessionOptions,
|
|
32
|
+
AttachSessionOptions,
|
|
33
|
+
MachineProviderEvent,
|
|
34
|
+
MachineProviderEventHandler,
|
|
35
|
+
EventedMachineProvider,
|
|
36
|
+
} from './MachineProvider.js';
|
|
37
|
+
import type {
|
|
38
|
+
MachineInfo,
|
|
39
|
+
Project,
|
|
40
|
+
Workspace,
|
|
41
|
+
WorkspaceSession,
|
|
42
|
+
InboxItem,
|
|
43
|
+
SessionStream,
|
|
44
|
+
} from '../types.js';
|
|
45
|
+
|
|
46
|
+
const STALE_DAYS = 30;
|
|
47
|
+
const LOCAL_MACHINE_ID = 'local';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert tmux-lite session to shared WorkspaceSession type
|
|
51
|
+
*/
|
|
52
|
+
function toWorkspaceSession(session: TmuxSession): WorkspaceSession {
|
|
53
|
+
return {
|
|
54
|
+
id: session.id,
|
|
55
|
+
name: session.name,
|
|
56
|
+
attached: session.attached,
|
|
57
|
+
createdAt: session.createdAt,
|
|
58
|
+
processTitle: session.processTitle,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* LocalMachineProvider - access local machine resources
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const provider = new LocalMachineProvider();
|
|
68
|
+
* const projects = await provider.listProjects();
|
|
69
|
+
* const workspaces = await provider.listWorkspaces('my-project');
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export class LocalMachineProvider implements EventedMachineProvider {
|
|
73
|
+
private eventHandlers: Set<MachineProviderEventHandler> = new Set();
|
|
74
|
+
private disposed = false;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get information about the local machine
|
|
78
|
+
*/
|
|
79
|
+
async getMachineInfo(): Promise<MachineInfo> {
|
|
80
|
+
return {
|
|
81
|
+
id: LOCAL_MACHINE_ID,
|
|
82
|
+
label: hostname() || 'Local',
|
|
83
|
+
isLocal: true,
|
|
84
|
+
status: 'connected',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List all projects on the local machine
|
|
90
|
+
*/
|
|
91
|
+
async listProjects(): Promise<Project[]> {
|
|
92
|
+
const projectNames = getAllProjectNames();
|
|
93
|
+
const currentProject = getCurrentProject();
|
|
94
|
+
|
|
95
|
+
return projectNames.map((name) => {
|
|
96
|
+
const config = readProjectConfig(name);
|
|
97
|
+
const workspacesDir = getProjectWorkspacesDir(name);
|
|
98
|
+
let workspaceCount = 0;
|
|
99
|
+
|
|
100
|
+
if (existsSync(workspacesDir)) {
|
|
101
|
+
workspaceCount = readdirSync(workspacesDir).filter((entry) => {
|
|
102
|
+
const path = join(workspacesDir, entry);
|
|
103
|
+
return existsSync(path) && readdirSync(path).length > 0;
|
|
104
|
+
}).length;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
name,
|
|
109
|
+
repository: config.repository,
|
|
110
|
+
workspaceCount,
|
|
111
|
+
isCurrent: name === currentProject,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* List workspaces for a project
|
|
118
|
+
*/
|
|
119
|
+
async listWorkspaces(projectName: string): Promise<Workspace[]> {
|
|
120
|
+
const workspacesDir = getProjectWorkspacesDir(projectName);
|
|
121
|
+
|
|
122
|
+
if (!existsSync(workspacesDir)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const workspaceNames = readdirSync(workspacesDir).filter((entry) => {
|
|
127
|
+
const path = join(workspacesDir, entry);
|
|
128
|
+
return existsSync(path) && readdirSync(path).length > 0;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Get all tmux-lite sessions
|
|
132
|
+
let allSessions: TmuxSession[] = [];
|
|
133
|
+
try {
|
|
134
|
+
allSessions = await tmuxListSessions();
|
|
135
|
+
} catch {
|
|
136
|
+
// Server might not be running, that's fine
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const workspaces: Workspace[] = [];
|
|
140
|
+
const now = new Date();
|
|
141
|
+
|
|
142
|
+
for (const name of workspaceNames) {
|
|
143
|
+
const workspacePath = join(workspacesDir, name);
|
|
144
|
+
const info = await getWorktreeInfo(workspacePath);
|
|
145
|
+
|
|
146
|
+
if (info) {
|
|
147
|
+
const daysSinceCommit = Math.floor(
|
|
148
|
+
(now.getTime() - info.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24)
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Find sessions for this workspace (name pattern: project:workspace:n)
|
|
152
|
+
const sessionPrefix = `${projectName}:${name}:`;
|
|
153
|
+
const workspaceSessions = allSessions
|
|
154
|
+
.filter(s => s.name.startsWith(sessionPrefix))
|
|
155
|
+
.map(toWorkspaceSession);
|
|
156
|
+
|
|
157
|
+
workspaces.push({
|
|
158
|
+
name: info.name,
|
|
159
|
+
path: info.path,
|
|
160
|
+
branch: info.branch,
|
|
161
|
+
ahead: info.ahead,
|
|
162
|
+
behind: info.behind,
|
|
163
|
+
uncommittedChanges: info.uncommittedChanges,
|
|
164
|
+
lastCommitDate: info.lastCommitDate,
|
|
165
|
+
isStale: daysSinceCommit > STALE_DAYS,
|
|
166
|
+
sessions: workspaceSessions,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return workspaces;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create a new session in a workspace
|
|
176
|
+
*/
|
|
177
|
+
async createSession(
|
|
178
|
+
projectName: string,
|
|
179
|
+
workspaceName: string,
|
|
180
|
+
options?: CreateSessionOptions
|
|
181
|
+
): Promise<string> {
|
|
182
|
+
const workspacesDir = getProjectWorkspacesDir(projectName);
|
|
183
|
+
const workspacePath = join(workspacesDir, workspaceName);
|
|
184
|
+
|
|
185
|
+
// Count existing sessions to generate unique name
|
|
186
|
+
let allSessions: TmuxSession[] = [];
|
|
187
|
+
try {
|
|
188
|
+
allSessions = await tmuxListSessions();
|
|
189
|
+
} catch {
|
|
190
|
+
// Ignore
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sessionPrefix = `${projectName}:${workspaceName}:`;
|
|
194
|
+
const existingCount = allSessions.filter(s => s.name.startsWith(sessionPrefix)).length;
|
|
195
|
+
const sessionName = `${sessionPrefix}${existingCount + 1}`;
|
|
196
|
+
|
|
197
|
+
const cwd = options?.cwd ?? workspacePath;
|
|
198
|
+
const session = await tmuxCreateSession(sessionName, cwd);
|
|
199
|
+
|
|
200
|
+
return session.id;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Attach to an existing session
|
|
205
|
+
*
|
|
206
|
+
* Returns a SessionStream for terminal I/O.
|
|
207
|
+
* For local sessions, this connects directly to the tmux-lite socket.
|
|
208
|
+
*/
|
|
209
|
+
async attachSession(
|
|
210
|
+
sessionId: string,
|
|
211
|
+
options: AttachSessionOptions
|
|
212
|
+
): Promise<SessionStream> {
|
|
213
|
+
await ensureServer();
|
|
214
|
+
|
|
215
|
+
// Get session info
|
|
216
|
+
const sessions = await tmuxListSessions();
|
|
217
|
+
const session = sessions.find(s => s.id === sessionId);
|
|
218
|
+
if (!session) {
|
|
219
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Connect to session socket
|
|
223
|
+
const socketPath = session.socketPath;
|
|
224
|
+
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
let dataHandler: ((data: Uint8Array) => void) | null = null;
|
|
227
|
+
let closeHandler: ((exitCode?: number) => void) | null = null;
|
|
228
|
+
let socket: Awaited<ReturnType<typeof Bun.connect>> | null = null;
|
|
229
|
+
let socketWriter: ReturnType<typeof createBufferedSocketWriter> | null = null;
|
|
230
|
+
let buffer = Buffer.alloc(0);
|
|
231
|
+
|
|
232
|
+
const stream: SessionStream = {
|
|
233
|
+
write(data: Uint8Array) {
|
|
234
|
+
if (socket) {
|
|
235
|
+
const { encodePTY } = require('../../lib/tmux-lite/protocol.js');
|
|
236
|
+
const frame = encodePTY(Buffer.from(data));
|
|
237
|
+
if (socketWriter) socketWriter.write(frame);
|
|
238
|
+
else socket.write(frame);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
resize(cols: number, rows: number) {
|
|
242
|
+
if (socket) {
|
|
243
|
+
const { encodeControl } = require('../../lib/tmux-lite/protocol.js');
|
|
244
|
+
const frame = encodeControl({ type: 'resize', cols, rows });
|
|
245
|
+
if (socketWriter) socketWriter.write(frame);
|
|
246
|
+
else socket.write(frame);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
detach() {
|
|
250
|
+
if (socket) {
|
|
251
|
+
const { encodeControl } = require('../../lib/tmux-lite/protocol.js');
|
|
252
|
+
const frame = encodeControl({ type: 'detach' });
|
|
253
|
+
if (socketWriter) socketWriter.write(frame);
|
|
254
|
+
else socket.write(frame);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
close() {
|
|
258
|
+
socket?.end();
|
|
259
|
+
socket = null;
|
|
260
|
+
socketWriter = null;
|
|
261
|
+
},
|
|
262
|
+
onData(handler: (data: Uint8Array) => void) {
|
|
263
|
+
dataHandler = handler;
|
|
264
|
+
},
|
|
265
|
+
onClose(handler: (exitCode?: number) => void) {
|
|
266
|
+
closeHandler = handler;
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
Bun.connect({
|
|
271
|
+
unix: socketPath,
|
|
272
|
+
socket: {
|
|
273
|
+
open(s) {
|
|
274
|
+
socket = s;
|
|
275
|
+
socketWriter = createBufferedSocketWriter(s);
|
|
276
|
+
// Send attach-init
|
|
277
|
+
const { encodeControl } = require('../../lib/tmux-lite/protocol.js');
|
|
278
|
+
socketWriter.write(encodeControl({
|
|
279
|
+
type: 'attach-init',
|
|
280
|
+
cols: options.cols,
|
|
281
|
+
rows: options.rows,
|
|
282
|
+
clientType: options.clientType,
|
|
283
|
+
}));
|
|
284
|
+
resolve(stream);
|
|
285
|
+
},
|
|
286
|
+
drain() {
|
|
287
|
+
socketWriter?.flush();
|
|
288
|
+
},
|
|
289
|
+
data(_, data) {
|
|
290
|
+
const { parseFrames, decodeControl, FrameType } = require('../../lib/tmux-lite/protocol.js');
|
|
291
|
+
buffer = Buffer.concat([buffer, Buffer.from(data)]);
|
|
292
|
+
|
|
293
|
+
let frames;
|
|
294
|
+
let remaining;
|
|
295
|
+
try {
|
|
296
|
+
const result = parseFrames(buffer);
|
|
297
|
+
frames = result.frames;
|
|
298
|
+
remaining = result.remaining;
|
|
299
|
+
} catch (err) {
|
|
300
|
+
// Protocol error - likely desync or corrupted data
|
|
301
|
+
const msg = err instanceof Error ? err.message : 'Frame parse error';
|
|
302
|
+
console.error(`[LocalMachineProvider] Frame parse error: ${msg}`);
|
|
303
|
+
closeHandler?.();
|
|
304
|
+
socket?.end();
|
|
305
|
+
socket = null;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// Copy remaining bytes - subarray references can become invalid when Bun reuses buffers
|
|
309
|
+
buffer = Buffer.from(remaining);
|
|
310
|
+
|
|
311
|
+
for (const frame of frames) {
|
|
312
|
+
if (frame.type === FrameType.CONTROL) {
|
|
313
|
+
const event = decodeControl(frame.payload);
|
|
314
|
+
if (event.type === 'exited') {
|
|
315
|
+
closeHandler?.(event.code);
|
|
316
|
+
socket?.end();
|
|
317
|
+
socket = null;
|
|
318
|
+
} else if (event.type === 'kicked') {
|
|
319
|
+
closeHandler?.();
|
|
320
|
+
socket?.end();
|
|
321
|
+
socket = null;
|
|
322
|
+
}
|
|
323
|
+
} else if (frame.type === FrameType.PTY) {
|
|
324
|
+
dataHandler?.(frame.payload);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
close() {
|
|
329
|
+
closeHandler?.();
|
|
330
|
+
socket = null;
|
|
331
|
+
},
|
|
332
|
+
error(_, e) {
|
|
333
|
+
reject(e);
|
|
334
|
+
},
|
|
335
|
+
connectError(_, e) {
|
|
336
|
+
reject(e);
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
}).catch(reject);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Detach from a session
|
|
345
|
+
*/
|
|
346
|
+
async detachSession(sessionId: string): Promise<void> {
|
|
347
|
+
// Detach is handled by the SessionStream.detach() method
|
|
348
|
+
// This is a no-op for local sessions
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get inbox notifications
|
|
353
|
+
*/
|
|
354
|
+
async getInbox(): Promise<InboxItem[]> {
|
|
355
|
+
try {
|
|
356
|
+
const items = await tmuxGetInbox();
|
|
357
|
+
return items.map(item => ({
|
|
358
|
+
id: item.id,
|
|
359
|
+
sessionId: item.sessionId,
|
|
360
|
+
sessionName: item.sessionName,
|
|
361
|
+
type: item.type,
|
|
362
|
+
timestamp: item.timestamp,
|
|
363
|
+
read: item.read,
|
|
364
|
+
context: item.context,
|
|
365
|
+
processTitle: item.processTitle,
|
|
366
|
+
exitCode: item.exitCode,
|
|
367
|
+
}));
|
|
368
|
+
} catch {
|
|
369
|
+
// Server might not be running
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Mark inbox item as read
|
|
376
|
+
*/
|
|
377
|
+
async markInboxRead(itemId: string): Promise<void> {
|
|
378
|
+
await tmuxMarkInboxRead(itemId);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Clear all inbox items
|
|
383
|
+
*/
|
|
384
|
+
async clearInbox(): Promise<void> {
|
|
385
|
+
await tmuxClearInbox();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Subscribe to provider events
|
|
390
|
+
*/
|
|
391
|
+
onEvent(handler: MachineProviderEventHandler): () => void {
|
|
392
|
+
this.eventHandlers.add(handler);
|
|
393
|
+
return () => this.eventHandlers.delete(handler);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Emit an event to all handlers
|
|
398
|
+
*/
|
|
399
|
+
private emit(event: MachineProviderEvent): void {
|
|
400
|
+
for (const handler of this.eventHandlers) {
|
|
401
|
+
handler(event);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Dispose of the provider
|
|
407
|
+
*/
|
|
408
|
+
dispose(): void {
|
|
409
|
+
if (this.disposed) return;
|
|
410
|
+
this.disposed = true;
|
|
411
|
+
this.eventHandlers.clear();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Singleton instance for convenience
|
|
417
|
+
*/
|
|
418
|
+
let localProvider: LocalMachineProvider | null = null;
|
|
419
|
+
|
|
420
|
+
export function getLocalMachineProvider(): LocalMachineProvider {
|
|
421
|
+
if (!localProvider) {
|
|
422
|
+
localProvider = new LocalMachineProvider();
|
|
423
|
+
}
|
|
424
|
+
return localProvider;
|
|
425
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MachineProvider Interface
|
|
3
|
+
*
|
|
4
|
+
* Abstraction for accessing machine resources (projects, workspaces, sessions).
|
|
5
|
+
* Implementations include:
|
|
6
|
+
* - LocalMachineProvider: Direct access to local machine
|
|
7
|
+
* - RemoteMachineProvider: Access via encrypted relay connection
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
MachineInfo,
|
|
12
|
+
Project,
|
|
13
|
+
Workspace,
|
|
14
|
+
InboxItem,
|
|
15
|
+
SessionStream,
|
|
16
|
+
} from '../types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for creating a new session
|
|
20
|
+
*/
|
|
21
|
+
export interface CreateSessionOptions {
|
|
22
|
+
/** Session name (user-friendly identifier) */
|
|
23
|
+
sessionName?: string;
|
|
24
|
+
/** Working directory for the session */
|
|
25
|
+
cwd?: string;
|
|
26
|
+
/** Shell to use (defaults to $SHELL or /bin/bash) */
|
|
27
|
+
shell?: string;
|
|
28
|
+
/** Environment variables */
|
|
29
|
+
env?: Record<string, string>;
|
|
30
|
+
/** Initial terminal size */
|
|
31
|
+
cols?: number;
|
|
32
|
+
rows?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for attaching to an existing session
|
|
37
|
+
*/
|
|
38
|
+
export interface AttachSessionOptions {
|
|
39
|
+
/** Terminal size */
|
|
40
|
+
cols: number;
|
|
41
|
+
rows: number;
|
|
42
|
+
/** Client type for session tracking */
|
|
43
|
+
clientType?: 'cli' | 'web';
|
|
44
|
+
/** Force attach (detach other clients) */
|
|
45
|
+
force?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* MachineProvider interface
|
|
50
|
+
*
|
|
51
|
+
* Provides a unified API for accessing machine resources,
|
|
52
|
+
* whether local or remote.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Local machine
|
|
57
|
+
* const local = new LocalMachineProvider();
|
|
58
|
+
* const projects = await local.listProjects();
|
|
59
|
+
*
|
|
60
|
+
* // Remote machine
|
|
61
|
+
* const remote = new RemoteMachineProvider(relayClient, machineId);
|
|
62
|
+
* const workspaces = await remote.listWorkspaces('my-project');
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export interface MachineProvider {
|
|
66
|
+
/**
|
|
67
|
+
* Get information about this machine
|
|
68
|
+
*/
|
|
69
|
+
getMachineInfo(): Promise<MachineInfo>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List all projects on this machine
|
|
73
|
+
*/
|
|
74
|
+
listProjects(): Promise<Project[]>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* List workspaces for a project
|
|
78
|
+
*
|
|
79
|
+
* @param projectName - Name of the project
|
|
80
|
+
*/
|
|
81
|
+
listWorkspaces(projectName: string): Promise<Workspace[]>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a new session in a workspace
|
|
85
|
+
*
|
|
86
|
+
* @param projectName - Name of the project
|
|
87
|
+
* @param workspaceName - Name of the workspace
|
|
88
|
+
* @param options - Session creation options
|
|
89
|
+
* @returns Session ID of the created session
|
|
90
|
+
*/
|
|
91
|
+
createSession(
|
|
92
|
+
projectName: string,
|
|
93
|
+
workspaceName: string,
|
|
94
|
+
options?: CreateSessionOptions
|
|
95
|
+
): Promise<string>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Attach to an existing session
|
|
99
|
+
*
|
|
100
|
+
* @param sessionId - ID of the session to attach to
|
|
101
|
+
* @param options - Attachment options
|
|
102
|
+
* @returns Stream for terminal I/O
|
|
103
|
+
*/
|
|
104
|
+
attachSession(
|
|
105
|
+
sessionId: string,
|
|
106
|
+
options: AttachSessionOptions
|
|
107
|
+
): Promise<SessionStream>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Detach from a session
|
|
111
|
+
*
|
|
112
|
+
* @param sessionId - ID of the session to detach from
|
|
113
|
+
*/
|
|
114
|
+
detachSession(sessionId: string): Promise<void>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get inbox notifications
|
|
118
|
+
*/
|
|
119
|
+
getInbox(): Promise<InboxItem[]>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Mark inbox item as read
|
|
123
|
+
*
|
|
124
|
+
* @param itemId - ID of the inbox item
|
|
125
|
+
*/
|
|
126
|
+
markInboxRead(itemId: string): Promise<void>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clear all inbox items
|
|
130
|
+
*/
|
|
131
|
+
clearInbox(): Promise<void>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Dispose of the provider and clean up resources
|
|
135
|
+
*/
|
|
136
|
+
dispose(): void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Events emitted by MachineProvider
|
|
141
|
+
*/
|
|
142
|
+
export type MachineProviderEvent =
|
|
143
|
+
| { type: 'connected' }
|
|
144
|
+
| { type: 'disconnected'; reason: string }
|
|
145
|
+
| { type: 'error'; error: Error }
|
|
146
|
+
| { type: 'inbox_updated'; items: InboxItem[] }
|
|
147
|
+
| { type: 'session_updated'; sessionId: string };
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Event handler for MachineProvider events
|
|
151
|
+
*/
|
|
152
|
+
export type MachineProviderEventHandler = (event: MachineProviderEvent) => void;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Extended MachineProvider with event support
|
|
156
|
+
*/
|
|
157
|
+
export interface EventedMachineProvider extends MachineProvider {
|
|
158
|
+
/**
|
|
159
|
+
* Subscribe to provider events
|
|
160
|
+
*
|
|
161
|
+
* @param handler - Event handler
|
|
162
|
+
* @returns Unsubscribe function
|
|
163
|
+
*/
|
|
164
|
+
onEvent(handler: MachineProviderEventHandler): () => void;
|
|
165
|
+
}
|