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,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Switch command implementation
|
|
3
|
+
* Handles both 'gssh switch project' and 'gssh switch [workspace-name]'
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync } from 'fs';
|
|
7
|
+
import {
|
|
8
|
+
readGlobalConfig,
|
|
9
|
+
readProjectConfig,
|
|
10
|
+
setCurrentProject,
|
|
11
|
+
getAllProjectNames,
|
|
12
|
+
getCurrentProject,
|
|
13
|
+
getProjectWorkspacesDir,
|
|
14
|
+
} from '../core/config.js';
|
|
15
|
+
import { selectItem } from '../utils/prompts.js';
|
|
16
|
+
import { logger } from '../utils/logger.js';
|
|
17
|
+
import { openWorkspaceShell } from '../core/shell.js';
|
|
18
|
+
import { getWorktreeInfo } from '../core/git.js';
|
|
19
|
+
import { SpacesError, NoProjectError } from '../types/errors.js';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { runCommandsInTerminal } from '../utils/run-commands.js';
|
|
22
|
+
import { fuzzyMatch } from '../utils/fuzzy-match.js';
|
|
23
|
+
import type {
|
|
24
|
+
WorkspaceCandidate,
|
|
25
|
+
RankedWorkspace,
|
|
26
|
+
} from '../types/workspace-fuzzy.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Switch to a different project
|
|
30
|
+
*/
|
|
31
|
+
export async function switchProject(projectNameArg?: string): Promise<void> {
|
|
32
|
+
const allProjects = getAllProjectNames();
|
|
33
|
+
|
|
34
|
+
if (allProjects.length === 0) {
|
|
35
|
+
throw new SpacesError(
|
|
36
|
+
'No projects found\n\nCreate a project first:\n gssh add project',
|
|
37
|
+
'USER_ERROR',
|
|
38
|
+
1
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let projectName: string;
|
|
43
|
+
|
|
44
|
+
if (projectNameArg) {
|
|
45
|
+
// Project name provided as argument
|
|
46
|
+
if (!allProjects.includes(projectNameArg)) {
|
|
47
|
+
throw new SpacesError(
|
|
48
|
+
`Project "${projectNameArg}" not found`,
|
|
49
|
+
'USER_ERROR',
|
|
50
|
+
1
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
projectName = projectNameArg;
|
|
54
|
+
} else {
|
|
55
|
+
// Select project using fzf
|
|
56
|
+
const currentProject = getCurrentProject();
|
|
57
|
+
const projectOptions = allProjects.map((name) => {
|
|
58
|
+
const config = readProjectConfig(name);
|
|
59
|
+
const indicator = name === currentProject ? ' (current)' : '';
|
|
60
|
+
return `${name} - ${config.repository}${indicator}`;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const selected = await selectItem(projectOptions, 'Select project:');
|
|
64
|
+
|
|
65
|
+
if (!selected) {
|
|
66
|
+
logger.info('Cancelled');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Extract project name from selection
|
|
71
|
+
projectName = selected.split(' - ')[0];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Set as current project
|
|
75
|
+
setCurrentProject(projectName);
|
|
76
|
+
logger.success(`Switched to project: ${projectName}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Switch to a workspace in the current project
|
|
81
|
+
*/
|
|
82
|
+
export async function switchWorkspace(
|
|
83
|
+
workspaceNameArg?: string,
|
|
84
|
+
options: {
|
|
85
|
+
noShell?: boolean;
|
|
86
|
+
force?: boolean;
|
|
87
|
+
} = {}
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
// Get current project
|
|
90
|
+
const currentProject = getCurrentProject();
|
|
91
|
+
if (!currentProject) {
|
|
92
|
+
throw new NoProjectError();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const projectConfig = readProjectConfig(currentProject);
|
|
96
|
+
const workspacesDir = getProjectWorkspacesDir(currentProject);
|
|
97
|
+
|
|
98
|
+
// Check if workspaces directory exists
|
|
99
|
+
if (!existsSync(workspacesDir)) {
|
|
100
|
+
throw new SpacesError(
|
|
101
|
+
`No workspaces found in project "${currentProject}"\n\nCreate a workspace first:\n gssh add`,
|
|
102
|
+
'USER_ERROR',
|
|
103
|
+
1
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get all workspace directories
|
|
108
|
+
const workspaces = readdirSync(workspacesDir).filter((entry) => {
|
|
109
|
+
const path = join(workspacesDir, entry);
|
|
110
|
+
return existsSync(path) && readdirSync(path).length > 0; // Not empty
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (workspaces.length === 0) {
|
|
114
|
+
throw new SpacesError(
|
|
115
|
+
`No workspaces found in project "${currentProject}"\n\nCreate a workspace first:\n gssh add`,
|
|
116
|
+
'USER_ERROR',
|
|
117
|
+
1
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let workspaceName: string;
|
|
122
|
+
|
|
123
|
+
if (workspaceNameArg) {
|
|
124
|
+
// Try exact match first (backward compatible)
|
|
125
|
+
if (workspaces.includes(workspaceNameArg)) {
|
|
126
|
+
workspaceName = workspaceNameArg;
|
|
127
|
+
} else {
|
|
128
|
+
// No exact match - try fuzzy matching
|
|
129
|
+
logger.debug(`No exact match for "${workspaceNameArg}", trying fuzzy match...`);
|
|
130
|
+
|
|
131
|
+
const candidates = await gatherWorkspaceCandidates(
|
|
132
|
+
workspacesDir,
|
|
133
|
+
workspaces
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const matches = fuzzyMatch(workspaceNameArg, candidates);
|
|
137
|
+
|
|
138
|
+
if (matches.length === 0) {
|
|
139
|
+
throw new SpacesError(
|
|
140
|
+
`No workspaces match "${workspaceNameArg}"\n\nAvailable workspaces:\n${workspaces.map(w => ' - ' + w).join('\n')}`,
|
|
141
|
+
'USER_ERROR',
|
|
142
|
+
1
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Rank matches
|
|
147
|
+
const ranked = rankMatches(matches);
|
|
148
|
+
|
|
149
|
+
// If only one match or force flag, use directly
|
|
150
|
+
if (ranked.length === 1 || options.force) {
|
|
151
|
+
workspaceName = ranked[0].workspace.name;
|
|
152
|
+
logger.info(`Fuzzy matched "${workspaceNameArg}" → ${workspaceName}`);
|
|
153
|
+
} else {
|
|
154
|
+
// Multiple matches - show interactive selection
|
|
155
|
+
const selected = await selectFromRanked(ranked, workspaceNameArg);
|
|
156
|
+
|
|
157
|
+
if (!selected) {
|
|
158
|
+
logger.info('Cancelled');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
workspaceName = selected.name;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Get workspace info for display
|
|
167
|
+
const workspaceOptions: string[] = [];
|
|
168
|
+
|
|
169
|
+
for (const workspace of workspaces) {
|
|
170
|
+
const workspacePath = join(workspacesDir, workspace);
|
|
171
|
+
const info = await getWorktreeInfo(workspacePath);
|
|
172
|
+
|
|
173
|
+
if (info) {
|
|
174
|
+
const statusParts: string[] = [];
|
|
175
|
+
|
|
176
|
+
// Add ahead/behind info
|
|
177
|
+
if (info.ahead > 0 || info.behind > 0) {
|
|
178
|
+
statusParts.push(`[${info.branch} +${info.ahead} -${info.behind}]`);
|
|
179
|
+
} else {
|
|
180
|
+
statusParts.push(`[${info.branch}]`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Add uncommitted changes
|
|
184
|
+
if (info.uncommittedChanges > 0) {
|
|
185
|
+
statusParts.push(`${info.uncommittedChanges} uncommitted`);
|
|
186
|
+
} else {
|
|
187
|
+
statusParts.push('clean');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const display = `${workspace.padEnd(30)} ${statusParts.join(' ')}`;
|
|
191
|
+
workspaceOptions.push(display);
|
|
192
|
+
} else {
|
|
193
|
+
workspaceOptions.push(workspace);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const selected = await selectItem(workspaceOptions, 'Select workspace:');
|
|
198
|
+
|
|
199
|
+
if (!selected) {
|
|
200
|
+
logger.info('Cancelled');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Extract workspace name (first part before padding)
|
|
205
|
+
workspaceName = selected.split(/\s+/)[0];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const workspacePath = join(workspacesDir, workspaceName);
|
|
209
|
+
|
|
210
|
+
// Switch to workspace
|
|
211
|
+
if (options.noShell) {
|
|
212
|
+
logger.success(`Workspace: ${workspacePath}`);
|
|
213
|
+
logger.log(`\nTo navigate:\n cd ${workspacePath}`);
|
|
214
|
+
} else {
|
|
215
|
+
// Open workspace shell
|
|
216
|
+
await openWorkspaceShell(
|
|
217
|
+
workspacePath,
|
|
218
|
+
currentProject,
|
|
219
|
+
projectConfig.repository,
|
|
220
|
+
false // never skip setup on switch
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Gather workspace candidates with metadata for fuzzy matching
|
|
227
|
+
*
|
|
228
|
+
* @param workspacesDir Path to workspaces directory
|
|
229
|
+
* @param workspaceNames Array of workspace names
|
|
230
|
+
* @returns Array of workspace candidates with metadata
|
|
231
|
+
*/
|
|
232
|
+
async function gatherWorkspaceCandidates(
|
|
233
|
+
workspacesDir: string,
|
|
234
|
+
workspaceNames: string[]
|
|
235
|
+
): Promise<WorkspaceCandidate[]> {
|
|
236
|
+
const candidates: WorkspaceCandidate[] = [];
|
|
237
|
+
|
|
238
|
+
for (const name of workspaceNames) {
|
|
239
|
+
const path = join(workspacesDir, name);
|
|
240
|
+
const info = await getWorktreeInfo(path);
|
|
241
|
+
|
|
242
|
+
if (info) {
|
|
243
|
+
candidates.push({
|
|
244
|
+
name: info.name,
|
|
245
|
+
path: info.path,
|
|
246
|
+
branch: info.branch,
|
|
247
|
+
ahead: info.ahead,
|
|
248
|
+
behind: info.behind,
|
|
249
|
+
uncommittedChanges: info.uncommittedChanges,
|
|
250
|
+
lastCommit: info.lastCommit,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return candidates;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Rank fuzzy matches with additional scoring
|
|
260
|
+
*
|
|
261
|
+
* Additional ranking factors:
|
|
262
|
+
* - Shorter workspace names get +5 bonus (easier to type)
|
|
263
|
+
*
|
|
264
|
+
* @param matches Fuzzy match results
|
|
265
|
+
* @returns Ranked workspace results
|
|
266
|
+
*/
|
|
267
|
+
function rankMatches(
|
|
268
|
+
matches: Array<{ item: WorkspaceCandidate; score: number; matchedIndices: number[] }>
|
|
269
|
+
): RankedWorkspace[] {
|
|
270
|
+
const ranked: RankedWorkspace[] = matches.map((match) => {
|
|
271
|
+
let finalScore = match.score;
|
|
272
|
+
|
|
273
|
+
// Bonus for shorter names (easier to remember/type)
|
|
274
|
+
if (match.item.name.length <= 15) {
|
|
275
|
+
finalScore += 5;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
workspace: match.item,
|
|
280
|
+
matchScore: match.score,
|
|
281
|
+
finalScore,
|
|
282
|
+
matchedIndices: match.matchedIndices,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Sort by final score
|
|
287
|
+
ranked.sort((a, b) => b.finalScore - a.finalScore);
|
|
288
|
+
|
|
289
|
+
return ranked;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Display ranked workspaces and prompt for selection
|
|
294
|
+
*
|
|
295
|
+
* @param ranked Ranked workspace results
|
|
296
|
+
* @param query Original query (for display)
|
|
297
|
+
* @returns Selected workspace or null if cancelled
|
|
298
|
+
*/
|
|
299
|
+
async function selectFromRanked(
|
|
300
|
+
ranked: RankedWorkspace[],
|
|
301
|
+
query: string
|
|
302
|
+
): Promise<WorkspaceCandidate | null> {
|
|
303
|
+
// Format each workspace for display
|
|
304
|
+
const choices = ranked.map((r) => formatWorkspaceChoice(r));
|
|
305
|
+
|
|
306
|
+
const selected = await selectItem(
|
|
307
|
+
choices,
|
|
308
|
+
`Multiple matches for "${query}":`
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (!selected) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse workspace name from selection (first part before padding)
|
|
316
|
+
const workspaceName = selected.split(/\s+/)[0];
|
|
317
|
+
const workspace = ranked.find((r) => r.workspace.name === workspaceName);
|
|
318
|
+
|
|
319
|
+
return workspace ? workspace.workspace : null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Format a ranked workspace for display in selection list
|
|
324
|
+
*
|
|
325
|
+
* Format: "name [branch +A -B] status"
|
|
326
|
+
* Example: "my-feature [main +2 -0] clean"
|
|
327
|
+
*
|
|
328
|
+
* @param ranked Ranked workspace
|
|
329
|
+
* @returns Formatted string for display
|
|
330
|
+
*/
|
|
331
|
+
function formatWorkspaceChoice(ranked: RankedWorkspace): string {
|
|
332
|
+
const ws = ranked.workspace;
|
|
333
|
+
const parts: string[] = [];
|
|
334
|
+
|
|
335
|
+
// Workspace name (padded to 30 chars for alignment)
|
|
336
|
+
parts.push(ws.name.padEnd(30));
|
|
337
|
+
|
|
338
|
+
// Branch info with ahead/behind
|
|
339
|
+
if (ws.ahead > 0 || ws.behind > 0) {
|
|
340
|
+
parts.push(`[${ws.branch} +${ws.ahead} -${ws.behind}]`);
|
|
341
|
+
} else {
|
|
342
|
+
parts.push(`[${ws.branch}]`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Uncommitted changes or clean status
|
|
346
|
+
if (ws.uncommittedChanges > 0) {
|
|
347
|
+
parts.push(`${ws.uncommittedChanges} uncommitted`);
|
|
348
|
+
} else {
|
|
349
|
+
parts.push('clean');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return parts.join(' ');
|
|
353
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux-lite daemon management commands
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* gssh tmux start - Start the tmux-lite server daemon
|
|
6
|
+
* gssh tmux stop - Stop the tmux-lite server daemon
|
|
7
|
+
* gssh tmux status - Show server status
|
|
8
|
+
* gssh tmux list - List sessions
|
|
9
|
+
* gssh tmux new [name] - Create and attach to a new session
|
|
10
|
+
* gssh tmux attach <id> - Attach to a session
|
|
11
|
+
* gssh tmux kill <id> - Kill a session
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { logger } from "../utils/logger.js";
|
|
15
|
+
import {
|
|
16
|
+
ensureServer,
|
|
17
|
+
isServerRunning,
|
|
18
|
+
getServerPid,
|
|
19
|
+
isProcessRunning,
|
|
20
|
+
cleanupStalePidFile,
|
|
21
|
+
getStatus,
|
|
22
|
+
killServer,
|
|
23
|
+
listSessions,
|
|
24
|
+
createSession,
|
|
25
|
+
killSession,
|
|
26
|
+
attach,
|
|
27
|
+
isNested,
|
|
28
|
+
getRouterSocket,
|
|
29
|
+
getPidFile,
|
|
30
|
+
PACKAGE_VERSION,
|
|
31
|
+
type Session,
|
|
32
|
+
} from "../lib/tmux-lite/cli.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format uptime in human-readable format
|
|
36
|
+
*/
|
|
37
|
+
function formatUptime(seconds: number): string {
|
|
38
|
+
if (seconds < 60) return `${seconds}s`;
|
|
39
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
40
|
+
const hours = Math.floor(seconds / 3600);
|
|
41
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
42
|
+
return `${hours}h ${mins}m`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Start the tmux-lite server daemon
|
|
47
|
+
*/
|
|
48
|
+
export async function startTmux(): Promise<void> {
|
|
49
|
+
// Clean up any stale PID file first
|
|
50
|
+
cleanupStalePidFile();
|
|
51
|
+
|
|
52
|
+
if (await isServerRunning()) {
|
|
53
|
+
const pid = getServerPid();
|
|
54
|
+
logger.info(`tmux-lite server already running${pid ? ` (pid ${pid})` : ""}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logger.log("Starting tmux-lite server...");
|
|
59
|
+
await ensureServer();
|
|
60
|
+
|
|
61
|
+
const pid = getServerPid();
|
|
62
|
+
logger.success(`tmux-lite server started${pid ? ` (pid ${pid})` : ""}`);
|
|
63
|
+
|
|
64
|
+
// Force exit since child process reference may keep event loop alive
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Stop the tmux-lite server daemon
|
|
70
|
+
*/
|
|
71
|
+
export async function stopTmux(options: { force?: boolean } = {}): Promise<void> {
|
|
72
|
+
// Clean up any stale PID file first
|
|
73
|
+
cleanupStalePidFile();
|
|
74
|
+
|
|
75
|
+
if (!(await isServerRunning())) {
|
|
76
|
+
logger.info("tmux-lite server not running");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for active sessions
|
|
81
|
+
try {
|
|
82
|
+
const sessions = await listSessions();
|
|
83
|
+
const activeSessions = sessions.filter((s) => !s.exitCode);
|
|
84
|
+
|
|
85
|
+
if (activeSessions.length > 0 && !options.force) {
|
|
86
|
+
logger.warning(
|
|
87
|
+
`Warning: ${activeSessions.length} active session(s) will be terminated`
|
|
88
|
+
);
|
|
89
|
+
logger.log("Sessions:");
|
|
90
|
+
for (const s of activeSessions) {
|
|
91
|
+
const status = s.attached ? "\x1b[32m●\x1b[0m" : "\x1b[90m○\x1b[0m";
|
|
92
|
+
logger.log(` ${status} ${s.id}: ${s.name}`);
|
|
93
|
+
}
|
|
94
|
+
logger.log("");
|
|
95
|
+
logger.log("Use --force to stop anyway");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore errors listing sessions
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.log("Stopping tmux-lite server...");
|
|
103
|
+
await killServer();
|
|
104
|
+
logger.success("tmux-lite server stopped");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Show tmux-lite server status
|
|
109
|
+
*/
|
|
110
|
+
export async function statusTmux(): Promise<void> {
|
|
111
|
+
// Clean up any stale PID file first
|
|
112
|
+
const wasStale = cleanupStalePidFile();
|
|
113
|
+
if (wasStale) {
|
|
114
|
+
logger.dim("(cleaned up stale PID file)");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const pid = getServerPid();
|
|
118
|
+
const pidAlive = pid !== null && isProcessRunning(pid);
|
|
119
|
+
const socketResponds = await isServerRunning();
|
|
120
|
+
|
|
121
|
+
// Build status output
|
|
122
|
+
const box = (lines: string[]) => {
|
|
123
|
+
const width = 44;
|
|
124
|
+
const top = "┌─ tmux-lite " + "─".repeat(width - 13) + "┐";
|
|
125
|
+
const bottom = "└" + "─".repeat(width) + "┘";
|
|
126
|
+
const padded = lines.map((l) => {
|
|
127
|
+
const visible = l.replace(/\x1b\[[0-9;]*m/g, ""); // Strip ANSI for length calc
|
|
128
|
+
const padding = width - visible.length;
|
|
129
|
+
return "│ " + l + " ".repeat(Math.max(0, padding - 1)) + "│";
|
|
130
|
+
});
|
|
131
|
+
return [top, ...padded, bottom].join("\n");
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (!socketResponds) {
|
|
135
|
+
// Server not running
|
|
136
|
+
const lines = [
|
|
137
|
+
`Status: \x1b[90m○ not running\x1b[0m`,
|
|
138
|
+
"",
|
|
139
|
+
`Run: \x1b[36mgssh tmux start\x1b[0m`,
|
|
140
|
+
];
|
|
141
|
+
logger.log(box(lines));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Server is running - get detailed status
|
|
146
|
+
try {
|
|
147
|
+
const status = await getStatus();
|
|
148
|
+
const statusIcon = "\x1b[32m●\x1b[0m"; // Green dot
|
|
149
|
+
const attachedStr =
|
|
150
|
+
status.attached > 0
|
|
151
|
+
? `(${status.attached} attached)`
|
|
152
|
+
: "";
|
|
153
|
+
|
|
154
|
+
const lines = [
|
|
155
|
+
`Status: ${statusIcon} running (pid ${status.pid})`,
|
|
156
|
+
`Version: ${status.version}`,
|
|
157
|
+
`Socket: ${getRouterSocket()}`,
|
|
158
|
+
`Sessions: ${status.sessions} total ${attachedStr}`,
|
|
159
|
+
`Uptime: ${formatUptime(status.uptime)}`,
|
|
160
|
+
];
|
|
161
|
+
logger.log(box(lines));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
// Fallback if status query fails
|
|
164
|
+
const lines = [
|
|
165
|
+
`Status: \x1b[32m●\x1b[0m running${pid ? ` (pid ${pid})` : ""}`,
|
|
166
|
+
`Version: ${PACKAGE_VERSION}`,
|
|
167
|
+
`Socket: ${getRouterSocket()}`,
|
|
168
|
+
];
|
|
169
|
+
logger.log(box(lines));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* List tmux-lite sessions
|
|
175
|
+
*/
|
|
176
|
+
export async function listTmux(): Promise<void> {
|
|
177
|
+
// Clean up any stale PID file first
|
|
178
|
+
cleanupStalePidFile();
|
|
179
|
+
|
|
180
|
+
if (!(await isServerRunning())) {
|
|
181
|
+
logger.info("tmux-lite server not running");
|
|
182
|
+
logger.dim("Run: gssh tmux start");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const sessions = await listSessions();
|
|
187
|
+
|
|
188
|
+
if (sessions.length === 0) {
|
|
189
|
+
logger.log("No sessions");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
logger.log("Sessions:");
|
|
194
|
+
for (const s of sessions) {
|
|
195
|
+
const age = Math.floor((Date.now() - s.createdAt) / 1000);
|
|
196
|
+
const ageStr =
|
|
197
|
+
age < 60
|
|
198
|
+
? `${age}s`
|
|
199
|
+
: age < 3600
|
|
200
|
+
? `${Math.floor(age / 60)}m`
|
|
201
|
+
: `${Math.floor(age / 3600)}h`;
|
|
202
|
+
const status = s.attached ? "\x1b[32m●\x1b[0m" : "\x1b[90m○\x1b[0m";
|
|
203
|
+
const exited = s.exitCode !== undefined ? ` \x1b[31m(exited ${s.exitCode})\x1b[0m` : "";
|
|
204
|
+
const title = s.processTitle ? ` \x1b[33m[${s.processTitle}]\x1b[0m` : "";
|
|
205
|
+
logger.log(` ${status} ${s.id}: ${s.name} (${ageStr})${title}${exited}`);
|
|
206
|
+
logger.dim(` ${s.cwd}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create and attach to a new tmux-lite session
|
|
212
|
+
*/
|
|
213
|
+
export async function newTmux(name?: string): Promise<void> {
|
|
214
|
+
// Check for nested session
|
|
215
|
+
if (isNested()) {
|
|
216
|
+
logger.error("Already inside a tmux-lite session");
|
|
217
|
+
logger.dim("Detach first with Ctrl+Esc");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Clean up any stale PID file first
|
|
222
|
+
cleanupStalePidFile();
|
|
223
|
+
|
|
224
|
+
// Ensure server is running
|
|
225
|
+
if (!(await isServerRunning())) {
|
|
226
|
+
logger.log("Starting tmux-lite server...");
|
|
227
|
+
await ensureServer();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const cwd = process.cwd();
|
|
231
|
+
const session = await createSession(name || "session", cwd);
|
|
232
|
+
|
|
233
|
+
logger.log(`Created session: ${session.name} (id: ${session.id})`);
|
|
234
|
+
logger.dim("Ctrl+Esc to detach\n");
|
|
235
|
+
|
|
236
|
+
const result = await attach(session, true);
|
|
237
|
+
|
|
238
|
+
if (result.type === "exited") {
|
|
239
|
+
process.exit(result.code);
|
|
240
|
+
} else if (result.type === "error") {
|
|
241
|
+
logger.error(result.message);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Attach to an existing tmux-lite session
|
|
248
|
+
*/
|
|
249
|
+
export async function attachTmux(id: string, options: { force?: boolean } = {}): Promise<void> {
|
|
250
|
+
// Check for nested session
|
|
251
|
+
if (isNested()) {
|
|
252
|
+
logger.error("Already inside a tmux-lite session");
|
|
253
|
+
logger.dim("Detach first with Ctrl+Esc");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Clean up any stale PID file first
|
|
258
|
+
cleanupStalePidFile();
|
|
259
|
+
|
|
260
|
+
if (!(await isServerRunning())) {
|
|
261
|
+
logger.error("tmux-lite server not running");
|
|
262
|
+
logger.dim("Run: gssh tmux start");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const sessions = await listSessions();
|
|
267
|
+
const session = sessions.find((s) => s.id === id || s.name === id);
|
|
268
|
+
|
|
269
|
+
if (!session) {
|
|
270
|
+
logger.error(`Session not found: ${id}`);
|
|
271
|
+
logger.dim("Run: gssh tmux list");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (session.attached && !options.force) {
|
|
276
|
+
logger.warning(`Session ${session.name} is attached elsewhere`);
|
|
277
|
+
logger.dim("Use --force to take over");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
logger.log(`Attaching to: ${session.name} (id: ${session.id})`);
|
|
282
|
+
logger.dim("Ctrl+Esc to detach\n");
|
|
283
|
+
|
|
284
|
+
const result = await attach(session, true);
|
|
285
|
+
|
|
286
|
+
if (result.type === "exited") {
|
|
287
|
+
process.exit(result.code);
|
|
288
|
+
} else if (result.type === "error") {
|
|
289
|
+
logger.error(result.message);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Kill a tmux-lite session
|
|
296
|
+
*/
|
|
297
|
+
export async function killTmux(id: string): Promise<void> {
|
|
298
|
+
// Clean up any stale PID file first
|
|
299
|
+
cleanupStalePidFile();
|
|
300
|
+
|
|
301
|
+
if (!(await isServerRunning())) {
|
|
302
|
+
logger.error("tmux-lite server not running");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const sessions = await listSessions();
|
|
307
|
+
const session = sessions.find((s) => s.id === id || s.name === id);
|
|
308
|
+
|
|
309
|
+
if (!session) {
|
|
310
|
+
logger.error(`Session not found: ${id}`);
|
|
311
|
+
logger.dim("Run: gssh tmux list");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await killSession(session.id);
|
|
316
|
+
logger.success(`Killed session: ${session.name} (id: ${session.id})`);
|
|
317
|
+
}
|