gitspace 0.2.0-rc.1 → 0.2.0-rc.3

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.
Files changed (28) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/README.md +11 -2
  3. package/bin/gssh +19 -3
  4. package/docs/QUICKSTART.md +11 -2
  5. package/docs/SITE_DOCS_FIGMA_MAKE.md +9 -7
  6. package/landing-page/src/components/docs/DocsContent.tsx +16 -10
  7. package/landing-page/src/components/landing/CTA.tsx +1 -1
  8. package/landing-page/src/components/landing/Features.tsx +2 -2
  9. package/landing-page/src/components/landing/UseCases.tsx +1 -1
  10. package/package.json +7 -6
  11. package/scripts/build.ts +14 -0
  12. package/src/commands/serve.ts +1 -1
  13. package/src/core/bundle.ts +2 -2
  14. package/src/index.ts +12 -11
  15. package/src/lib/tmux-lite/cli.ts +21 -6
  16. package/src/lib/tmux-lite/server-lifecycle.test.ts +208 -0
  17. package/src/lib/tmux-lite/server.ts +5 -1
  18. package/src/shared/providers/RemoteMachineProvider.ts +1 -6
  19. package/src/tui/hooks/useInboxTUI.ts +6 -5
  20. package/src/tui/hooks/useRemoteMachines.ts +2 -3
  21. package/npm/darwin-arm64/bin/gssh +0 -0
  22. package/npm/darwin-arm64/package.json +0 -20
  23. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  24. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  25. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  26. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  27. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  28. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
@@ -15,7 +15,9 @@
15
15
  "Bash(xxd:*)",
16
16
  "WebFetch(domain:ast-grep.github.io)",
17
17
  "WebFetch(domain:developers.cloudflare.com)",
18
- "WebFetch(domain:gitspace.sh)"
18
+ "WebFetch(domain:gitspace.sh)",
19
+ "Bash(/Users/bradleat/spaces/spaces/workspaces/remote-space/dist/gssh-darwin-arm64:*)",
20
+ "Bash(npx wrangler pages deploy:*)"
19
21
  ]
20
22
  }
21
23
  }
package/README.md CHANGED
@@ -20,7 +20,6 @@ The following tools must be installed and available in your PATH:
20
20
  - [GitHub CLI (`gh`)](https://cli.github.com/) - for listing repositories
21
21
  - [Git](https://git-scm.com/) - for worktree management
22
22
  - [jq](https://stedolan.github.io/jq/) - for JSON processing
23
- - [Bun](https://bun.sh) - runtime for the CLI
24
23
 
25
24
  **GitHub Authentication**: You must authenticate the GitHub CLI before using GitSpace:
26
25
 
@@ -31,7 +30,17 @@ gh auth login
31
30
  ## Installation
32
31
 
33
32
  ```bash
34
- bun install -g https://github.com/inkibra/gitspace.sh
33
+ # npm
34
+ npm install -g gitspace
35
+
36
+ # bun
37
+ bun install -g gitspace
38
+
39
+ # pnpm
40
+ pnpm install -g gitspace
41
+
42
+ # yarn
43
+ yarn global add gitspace
35
44
 
36
45
  # Verify installation
37
46
  gssh --version
package/bin/gssh CHANGED
@@ -12,7 +12,23 @@
12
12
 
13
13
  set -e
14
14
 
15
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ # Resolve symlinks to find the real script location
16
+ resolve_symlink() {
17
+ file="$1"
18
+ while [ -L "$file" ]; do
19
+ dir="$(cd "$(dirname "$file")" && pwd)"
20
+ file="$(readlink "$file")"
21
+ # Handle relative symlinks
22
+ case "$file" in
23
+ /*) ;;
24
+ *) file="$dir/$file" ;;
25
+ esac
26
+ done
27
+ echo "$file"
28
+ }
29
+
30
+ REAL_SCRIPT="$(resolve_symlink "$0")"
31
+ SCRIPT_DIR="$(cd "$(dirname "$REAL_SCRIPT")" && pwd)"
16
32
  ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
17
33
 
18
34
  # Get platform identifier
@@ -50,8 +66,8 @@ if [ -n "$PLATFORM" ]; then
50
66
  done
51
67
  fi
52
68
 
53
- # 3. Fall back to bun for development
54
- if command -v bun >/dev/null 2>&1; then
69
+ # 3. Fall back to bun for development (only if src/index.ts exists)
70
+ if [ -f "$ROOT_DIR/src/index.ts" ] && command -v bun >/dev/null 2>&1; then
55
71
  exec bun "$ROOT_DIR/src/index.ts" "$@"
56
72
  fi
57
73
 
@@ -8,7 +8,6 @@ Get up and running with GitSpace in 5 minutes.
8
8
 
9
9
  Install these tools first:
10
10
 
11
- - [Bun](https://bun.sh) - JavaScript runtime
12
11
  - [Git](https://git-scm.com/) - Version control
13
12
  - [GitHub CLI](https://cli.github.com/) - `gh auth login` before using GitSpace
14
13
 
@@ -17,7 +16,17 @@ Install these tools first:
17
16
  ## Installation
18
17
 
19
18
  ```bash
20
- bun install -g https://github.com/inkibra/gitspace.sh
19
+ # npm
20
+ npm install -g gitspace
21
+
22
+ # bun
23
+ bun install -g gitspace
24
+
25
+ # pnpm
26
+ pnpm install -g gitspace
27
+
28
+ # yarn
29
+ yarn global add gitspace
21
30
 
22
31
  # Verify
23
32
  gssh --version
@@ -53,14 +53,16 @@ Prereqs (list these on docs site):
53
53
  - gh (GitHub CLI)
54
54
  - git
55
55
  - jq
56
- - bun
57
56
 
58
57
  Optional prereqs:
59
58
  - cloudflared (for `gssh host` commands)
60
59
  - Linear API key (for Linear integration)
61
60
 
62
- Install:
63
- - `bun install -g https://github.com/inkibra/gitspace.sh`
61
+ Install (any package manager):
62
+ - `npm install -g gitspace`
63
+ - `bun install -g gitspace`
64
+ - `pnpm install -g gitspace`
65
+ - `yarn global add gitspace`
64
66
  - Verify: `gssh --version`
65
67
 
66
68
  ---
@@ -700,7 +702,7 @@ Feature bullets:
700
702
  Short code example:
701
703
  ```bash
702
704
  # Install
703
- bun install -g https://github.com/inkibra/gitspace.sh
705
+ npm install -g gitspace
704
706
 
705
707
  # Launch the TUI
706
708
  spaces
@@ -767,7 +769,7 @@ SECTION: Quick Start
767
769
 
768
770
  ```bash
769
771
  # 1. Install
770
- bun install -g https://github.com/inkibra/gitspace.sh
772
+ npm install -g gitspace
771
773
 
772
774
  # 2. Create identity
773
775
  gssh identity init
@@ -788,7 +790,7 @@ gssh serve
788
790
 
789
791
  ```bash
790
792
  # Install
791
- bun install -g https://github.com/inkibra/gitspace.sh
793
+ npm install -g gitspace
792
794
 
793
795
  # Authenticate GitHub
794
796
  gh auth login
@@ -819,7 +821,7 @@ Optional:
819
821
  ### Install GitSpace
820
822
 
821
823
  ```bash
822
- bun install -g https://github.com/inkibra/gitspace.sh
824
+ npm install -g gitspace
823
825
  gssh --version
824
826
  ```
825
827
 
@@ -105,8 +105,10 @@ export function DocsContent({ section }: { section: string }) {
105
105
  <h1 className="text-4xl font-bold mb-6">Quick Start</h1>
106
106
 
107
107
  <h3 className="text-xl font-semibold text-white mb-4">5-Minute Setup with gitspace.sh</h3>
108
- <CodeBlock code={`# 1. Install
109
- bun install -g https://github.com/inkibra/gitspace.sh
108
+ <CodeBlock code={`# 1. Install (pick your package manager)
109
+ npm install -g gitspace
110
+ # or: bun install -g gitspace
111
+ # or: pnpm install -g gitspace
110
112
 
111
113
  # 2. Create identity
112
114
  gssh identity init
@@ -124,7 +126,7 @@ gssh serve
124
126
 
125
127
  <h3 className="text-xl font-semibold text-white mb-4 mt-12">Local-Only Quick Start</h3>
126
128
  <CodeBlock code={`# Install
127
- bun install -g https://github.com/inkibra/gitspace.sh
129
+ npm install -g gitspace
128
130
 
129
131
  # Authenticate GitHub
130
132
  gh auth login
@@ -146,19 +148,23 @@ gssh add my-feature # Create a workspace`} multiLine />
146
148
  <h3 className="text-xl font-semibold text-white mb-4">Prerequisites</h3>
147
149
  <p className="text-zinc-400 mb-4">Required:</p>
148
150
  <ul className="list-disc list-inside space-y-2 text-zinc-400 mb-8 ml-2">
149
- <li><a href="https://bun.sh" className="text-green-400 hover:underline">Bun</a> - JavaScript runtime</li>
150
151
  <li><a href="https://git-scm.com/" className="text-green-400 hover:underline">Git</a> - Version control</li>
151
152
  <li><a href="https://cli.github.com/" className="text-green-400 hover:underline">GitHub CLI</a> - <code className="text-zinc-300">gh auth login</code> before using GitSpace</li>
152
153
  <li><a href="https://stedolan.github.io/jq/" className="text-green-400 hover:underline">jq</a> - JSON processing</li>
153
154
  </ul>
154
155
 
155
- <p className="text-zinc-400 mb-4">Optional:</p>
156
- <ul className="list-disc list-inside space-y-2 text-zinc-400 mb-8 ml-2">
157
- <li><a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" className="text-green-400 hover:underline">cloudflared</a> - For <code className="text-zinc-300">gssh host</code> commands</li>
158
- </ul>
159
-
160
156
  <h3 className="text-xl font-semibold text-white mb-4">Install GitSpace</h3>
161
- <CodeBlock code="bun install -g https://github.com/inkibra/gitspace.sh" />
157
+ <CodeBlock code={`# npm
158
+ npm install -g gitspace
159
+
160
+ # bun
161
+ bun install -g gitspace
162
+
163
+ # pnpm
164
+ pnpm install -g gitspace
165
+
166
+ # yarn
167
+ yarn global add gitspace`} multiLine />
162
168
  <CodeBlock code="gssh --version" />
163
169
 
164
170
  <h3 className="text-xl font-semibold text-white mb-4">Authenticate GitHub CLI</h3>
@@ -36,7 +36,7 @@ export function CTA() {
36
36
  <div className="space-y-2 font-mono text-sm">
37
37
  <div className="flex">
38
38
  <span className="text-green-500 mr-2">$</span>
39
- <span className="text-white">bun install -g https://github.com/inkibra/gitspace.sh</span>
39
+ <span className="text-white">npm install -g gitspace</span>
40
40
  </div>
41
41
  <div className="text-zinc-500 text-xs py-1">...</div>
42
42
  <div className="flex">
@@ -47,7 +47,7 @@ export function Features() {
47
47
  <TerminalWindow title="bash">
48
48
  <div className="space-y-2 font-mono text-sm">
49
49
  <div className="text-zinc-500"># Install</div>
50
- <div className="text-zinc-400">$ bun install -g https://github.com/inkibra/gitspace.sh</div>
50
+ <div className="text-zinc-400">$ npm install -g gitspace</div>
51
51
  <br />
52
52
  <div className="text-zinc-500"># Launch the TUI</div>
53
53
  <div className="text-zinc-400">$ gssh</div>
@@ -166,7 +166,7 @@ export function Features() {
166
166
  <div>
167
167
  <TerminalWindow title="bash">
168
168
  <div className="space-y-2 font-mono text-sm">
169
- <div className="text-zinc-400">$ gitspace stack</div>
169
+ <div className="text-zinc-400">$ gssh stack</div>
170
170
  <div className="text-blue-400 animate-pulse">Analyzing 47 commits across 30 files...</div>
171
171
  <br />
172
172
  <div className="text-zinc-300">Suggested stack:</div>
@@ -4,7 +4,7 @@ import { Card, CardContent } from "../../app/components/ui/card";
4
4
  export function UseCases() {
5
5
  const testimonials = [
6
6
  {
7
- quote: "I run 3-4 Claude agents in parallel now. Each in its own space. I check progress from my phone while making dinner. When they're done, git stack turns the mess into reviewable PRs.",
7
+ quote: "I run 3-4 Claude agents in parallel now. Each in its own space. I check progress from my phone while making dinner. Context switching between tasks is instant.",
8
8
  author: "Solo founder",
9
9
  role: "Shipping 3x faster",
10
10
  stars: 5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitspace",
3
- "version": "0.2.0-rc.1",
3
+ "version": "0.2.0-rc.3",
4
4
  "description": "CLI for managing GitHub workspaces with git worktrees and secure remote terminal access",
5
5
  "bin": {
6
6
  "gssh": "./bin/gssh"
@@ -17,10 +17,10 @@
17
17
  "relay": "bun src/relay/index.ts"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@gitspace/darwin-arm64": "0.2.0-rc.1",
21
- "@gitspace/darwin-x64": "0.2.0-rc.1",
22
- "@gitspace/linux-x64": "0.2.0-rc.1",
23
- "@gitspace/linux-arm64": "0.2.0-rc.1"
20
+ "@gitspace/darwin-arm64": "0.2.0-rc.3",
21
+ "@gitspace/darwin-x64": "0.2.0-rc.3",
22
+ "@gitspace/linux-x64": "0.2.0-rc.3",
23
+ "@gitspace/linux-arm64": "0.2.0-rc.3"
24
24
  },
25
25
  "keywords": [
26
26
  "cli",
@@ -70,5 +70,6 @@
70
70
  },
71
71
  "engines": {
72
72
  "bun": ">=1.3.5"
73
- }
73
+ },
74
+ "packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
74
75
  }
package/scripts/build.ts CHANGED
@@ -23,6 +23,7 @@ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from
23
23
  const ROOT = join(import.meta.dir, "..");
24
24
  const WEB_DIST = join(ROOT, "src/web/dist");
25
25
  const EMBEDDED_ASSETS_PATH = join(ROOT, "src/relay/embedded-assets.generated.js");
26
+ const VERSION_PATH = join(ROOT, "src/version.generated.ts");
26
27
  const DIST_DIR = join(ROOT, "dist");
27
28
  const NPM_DIR = join(ROOT, "npm");
28
29
 
@@ -41,6 +42,18 @@ const TARGETS = {
41
42
 
42
43
  type TargetKey = keyof typeof TARGETS;
43
44
 
45
+ /** Generate version file with current version */
46
+ function generateVersionFile() {
47
+ const code = `/**
48
+ * AUTO-GENERATED - DO NOT EDIT
49
+ * Generated by: bun scripts/build.ts
50
+ */
51
+ export const VERSION = '${VERSION}';
52
+ `;
53
+ writeFileSync(VERSION_PATH, code);
54
+ console.log(`✓ Generated version file (${VERSION})`);
55
+ }
56
+
44
57
  /** Restore stub file before web build */
45
58
  function restoreStub() {
46
59
  const stub = `/**
@@ -232,6 +245,7 @@ async function main() {
232
245
 
233
246
  console.log(`🚀 GitSpace CLI Build v${VERSION}\n`);
234
247
 
248
+ generateVersionFile();
235
249
  await buildWeb();
236
250
  await generateEmbeddedAssets();
237
251
 
@@ -58,6 +58,7 @@ import {
58
58
  sendShutdownCommand,
59
59
  getServeLogFile,
60
60
  setAccessCommandHandler,
61
+ ensureServeDaemonDir,
61
62
  type StatusResponse,
62
63
  } from '../serve/daemon.js';
63
64
 
@@ -1214,7 +1215,6 @@ export async function serveStart(options: {
1214
1215
 
1215
1216
  // Write output to log file for debugging
1216
1217
  const logFile = getServeLogFile();
1217
- const { ensureServeDaemonDir } = await import('../serve/daemon.js');
1218
1218
  ensureServeDaemonDir();
1219
1219
 
1220
1220
  // Truncate log file at start
@@ -15,6 +15,8 @@ import {
15
15
  } from 'fs';
16
16
  import { join, basename, resolve, sep } from 'path';
17
17
  import { tmpdir } from 'os';
18
+ import { exec } from 'child_process';
19
+ import { promisify } from 'util';
18
20
  import { SpacesError } from '../types/errors.js';
19
21
  import { logger } from '../utils/logger.js';
20
22
  import type { SpacesBundle, LoadedBundle } from '../types/bundle.js';
@@ -140,8 +142,6 @@ export async function loadBundleFromUrl(url: string): Promise<LoadedBundle> {
140
142
  writeFileSync(zipPath, Buffer.from(arrayBuffer));
141
143
 
142
144
  // Extract using unzip command
143
- const { exec } = await import('child_process');
144
- const { promisify } = await import('util');
145
145
  const execAsync = promisify(exec);
146
146
 
147
147
  await execAsync(`unzip -q "${zipPath}" -d "${tempDir}"`);
package/src/index.ts CHANGED
@@ -9,6 +9,17 @@ import { Command } from 'commander'
9
9
  import { readFileSync } from 'fs'
10
10
  import { join } from 'path'
11
11
  import { isFirstTimeSetup, initializeSpaces } from './core/config.js'
12
+ import { VERSION as GENERATED_VERSION } from './version.generated.js'
13
+
14
+ // Read version from package.json in dev, fall back to generated for compiled binary
15
+ let VERSION = GENERATED_VERSION
16
+ try {
17
+ const pkgPath = join(import.meta.dir, '../package.json')
18
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
19
+ VERSION = pkg.version
20
+ } catch {
21
+ // Compiled binary - use generated version
22
+ }
12
23
  import { logger } from './utils/logger.js'
13
24
  import { SpacesError } from './types/errors.js'
14
25
  import { addProject, addWorkspace } from './commands/add.js'
@@ -31,21 +42,11 @@ import { showStatus } from './commands/status.js'
31
42
 
32
43
  const program = new Command()
33
44
 
34
- // Read version from package.json
35
- let version = '0.0.0'
36
- try {
37
- const pkgPath = join(import.meta.dir, '../package.json')
38
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
39
- version = pkg.version
40
- } catch {
41
- // Fallback for compiled binary
42
- }
43
-
44
45
  // Package info
45
46
  program
46
47
  .name('gssh')
47
48
  .description('GitSpace CLI - Manage GitHub workspaces with secure remote terminal access')
48
- .version(version)
49
+ .version(VERSION)
49
50
 
50
51
  // First-time setup check
51
52
  async function checkFirstTimeSetup(): Promise<void> {
@@ -605,8 +605,10 @@ async function main() {
605
605
  console.log("Starting server...");
606
606
  spawn({
607
607
  cmd: getServerCommand(),
608
- stdout: "inherit",
609
- stderr: "inherit",
608
+ // Use "ignore" so server doesn't inherit CLI's stdout/stderr
609
+ // This allows CLI to exit cleanly when piped
610
+ stdout: "ignore",
611
+ stderr: "ignore",
610
612
  });
611
613
  await Bun.sleep(300);
612
614
  if (!(await isServerRunning())) {
@@ -787,10 +789,23 @@ In session:
787
789
  }
788
790
  }
789
791
 
792
+ // Commands that are non-interactive and should exit immediately after completion
793
+ const NON_INTERACTIVE_COMMANDS = new Set([
794
+ "list", "ls", "kill", "kill-server", "inbox", "inbox-clear", "status", "version"
795
+ ]);
796
+
790
797
  // Only run CLI when executed directly, not when imported as a module
791
798
  if (import.meta.main) {
792
- main().catch(e => {
793
- console.error(e.message);
794
- process.exit(1);
795
- });
799
+ main()
800
+ .then(() => {
801
+ // Force exit after non-interactive commands complete
802
+ // Some socket references may keep the event loop alive otherwise
803
+ if (NON_INTERACTIVE_COMMANDS.has(cmd)) {
804
+ process.exit(0);
805
+ }
806
+ })
807
+ .catch(e => {
808
+ console.error(e.message);
809
+ process.exit(1);
810
+ });
796
811
  }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * E2E tests for tmux-lite server lifecycle
3
+ *
4
+ * Tests server start, stop, and restart scenarios including
5
+ * proper cleanup of socket files.
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
8
+ import { spawn } from "bun";
9
+ import { existsSync, unlinkSync, mkdirSync, rmSync } from "fs";
10
+ import { join } from "path";
11
+
12
+ // Use the same paths as --test mode in cli.ts and server.ts
13
+ const TEST_SOCKET = "/tmp/tmux-lite-test.sock";
14
+ const TEST_SESSION_DIR = "/tmp/tmux-lite-test";
15
+ const CLI_SCRIPT = join(import.meta.dir, "cli.ts");
16
+
17
+ /**
18
+ * Helper to run CLI commands in test mode
19
+ */
20
+ async function runCli(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
21
+ const proc = spawn({
22
+ cmd: ["bun", "run", CLI_SCRIPT, command, "--test"],
23
+ stdout: "pipe",
24
+ stderr: "pipe",
25
+ });
26
+
27
+ const stdout = await new Response(proc.stdout).text();
28
+ const stderr = await new Response(proc.stderr).text();
29
+ const exitCode = await proc.exited;
30
+
31
+ // Give server time to initialize after CLI returns
32
+ await Bun.sleep(500);
33
+
34
+ return { stdout, stderr, exitCode };
35
+ }
36
+
37
+ /**
38
+ * Helper to check if server is running by checking socket exists and responds
39
+ */
40
+ async function isServerRunning(): Promise<boolean> {
41
+ if (!existsSync(TEST_SOCKET)) return false;
42
+
43
+ try {
44
+ const result = await runCli("list");
45
+ // If we can list sessions, server is running
46
+ return result.exitCode === 0;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Helper to wait for a condition with timeout
54
+ */
55
+ async function waitFor(
56
+ condition: () => Promise<boolean>,
57
+ timeoutMs: number = 5000,
58
+ intervalMs: number = 100
59
+ ): Promise<boolean> {
60
+ const start = Date.now();
61
+ while (Date.now() - start < timeoutMs) {
62
+ if (await condition()) return true;
63
+ await Bun.sleep(intervalMs);
64
+ }
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Force cleanup any leftover test artifacts
70
+ */
71
+ function forceCleanup(): void {
72
+ try { unlinkSync(TEST_SOCKET); } catch {}
73
+ try { rmSync(TEST_SESSION_DIR, { recursive: true, force: true }); } catch {}
74
+ }
75
+
76
+ describe("tmux-lite server lifecycle", () => {
77
+ beforeEach(() => {
78
+ // Ensure clean state before each test
79
+ forceCleanup();
80
+ mkdirSync(TEST_SESSION_DIR, { recursive: true });
81
+ });
82
+
83
+ afterEach(async () => {
84
+ // Kill server if still running and cleanup
85
+ try {
86
+ await runCli("kill-server");
87
+ await Bun.sleep(200); // Wait for cleanup
88
+ } catch {}
89
+ forceCleanup();
90
+ });
91
+
92
+ describe("server start", () => {
93
+ it("should start server and create socket file", async () => {
94
+ // Socket should not exist initially
95
+ expect(existsSync(TEST_SOCKET)).toBe(false);
96
+
97
+ // Start server by running any command (list auto-starts)
98
+ const result = await runCli("list");
99
+
100
+ // Wait for server to be ready
101
+ const started = await waitFor(async () => existsSync(TEST_SOCKET));
102
+ expect(started).toBe(true);
103
+
104
+ // Server should be running
105
+ const running = await isServerRunning();
106
+ expect(running).toBe(true);
107
+ });
108
+
109
+ it("should not fail if server is already running", async () => {
110
+ // Start server first time
111
+ await runCli("list");
112
+ await waitFor(async () => existsSync(TEST_SOCKET));
113
+
114
+ // Run another command - should not fail
115
+ const result = await runCli("list");
116
+ expect(result.exitCode).toBe(0);
117
+ });
118
+ });
119
+
120
+ describe("server stop", () => {
121
+ it("should stop server and remove socket file", async () => {
122
+ // Start server
123
+ await runCli("list");
124
+ await waitFor(async () => existsSync(TEST_SOCKET));
125
+ expect(existsSync(TEST_SOCKET)).toBe(true);
126
+
127
+ // Stop server
128
+ await runCli("kill-server");
129
+
130
+ // Wait for socket to be cleaned up
131
+ const cleaned = await waitFor(async () => !existsSync(TEST_SOCKET), 2000);
132
+ expect(cleaned).toBe(true);
133
+
134
+ // Socket file should be removed
135
+ expect(existsSync(TEST_SOCKET)).toBe(false);
136
+ });
137
+
138
+ it("should not fail if server is not running", async () => {
139
+ // Ensure server is not running
140
+ expect(existsSync(TEST_SOCKET)).toBe(false);
141
+
142
+ // Stop should handle gracefully
143
+ const result = await runCli("kill-server");
144
+ // CLI shows "Server not running" but exits cleanly
145
+ expect(result.exitCode).toBe(0);
146
+ });
147
+ });
148
+
149
+ describe("server restart cycle", () => {
150
+ it("should successfully restart after stop (socket cleanup regression test)", async () => {
151
+ // This is the main regression test for the socket cleanup fix
152
+
153
+ // Start server
154
+ await runCli("list");
155
+ const started1 = await waitFor(async () => existsSync(TEST_SOCKET));
156
+ expect(started1).toBe(true);
157
+
158
+ // Stop server
159
+ await runCli("kill-server");
160
+ const stopped = await waitFor(async () => !existsSync(TEST_SOCKET), 2000);
161
+ expect(stopped).toBe(true);
162
+
163
+ // Start server again - THIS WAS FAILING before the fix
164
+ // because the socket file wasn't being cleaned up
165
+ await runCli("list");
166
+ const started2 = await waitFor(async () => existsSync(TEST_SOCKET));
167
+ expect(started2).toBe(true);
168
+
169
+ // Verify server is actually running
170
+ const running = await isServerRunning();
171
+ expect(running).toBe(true);
172
+ });
173
+
174
+ it("should handle multiple restart cycles", async () => {
175
+ for (let i = 0; i < 3; i++) {
176
+ // Start
177
+ await runCli("list");
178
+ const started = await waitFor(async () => existsSync(TEST_SOCKET));
179
+ expect(started).toBe(true);
180
+
181
+ // Verify running
182
+ const running = await isServerRunning();
183
+ expect(running).toBe(true);
184
+
185
+ // Stop
186
+ await runCli("kill-server");
187
+ const stopped = await waitFor(async () => !existsSync(TEST_SOCKET), 2000);
188
+ expect(stopped).toBe(true);
189
+ }
190
+ }, 15000); // Increase timeout for 3 cycles
191
+ });
192
+
193
+ describe("stale socket handling", () => {
194
+ it("should clean up stale socket on server start", async () => {
195
+ // Create a stale socket file (simulating crashed server)
196
+ Bun.write(TEST_SOCKET, "stale");
197
+ expect(existsSync(TEST_SOCKET)).toBe(true);
198
+
199
+ // Start server - should clean up stale socket and start fresh
200
+ await runCli("list");
201
+ const started = await waitFor(async () => {
202
+ // Socket exists AND server responds
203
+ return await isServerRunning();
204
+ });
205
+ expect(started).toBe(true);
206
+ });
207
+ });
208
+ });
@@ -1161,7 +1161,11 @@ Bun.listen({
1161
1161
  res = { type: "ok" };
1162
1162
  if (socketState.writer) socketState.writer.write(encodeRouterMessage(res));
1163
1163
  else socket.write(encodeRouterMessage(res));
1164
- setTimeout(() => process.exit(0), 100);
1164
+ // Clean up socket file after sending response, before exit
1165
+ setTimeout(() => {
1166
+ try { unlinkSync(ROUTER_SOCKET); } catch {}
1167
+ process.exit(0);
1168
+ }, 100);
1165
1169
  return;
1166
1170
 
1167
1171
  case "inbox":
@@ -5,6 +5,7 @@
5
5
  * through the relay server with encrypted communication.
6
6
  */
7
7
 
8
+ import WebSocket from 'ws';
8
9
  import type {
9
10
  MachineProvider,
10
11
  CreateSessionOptions,
@@ -18,9 +19,6 @@ import type {
18
19
  SessionStream,
19
20
  } from '../types.js';
20
21
 
21
- // Use dynamic import for WebSocket to support both Node and browser
22
- // In Node, we use the 'ws' package; in browser, we use native WebSocket
23
-
24
22
  /**
25
23
  * Common WebSocket interface for both Node.js (ws package) and browser environments
26
24
  */
@@ -86,9 +84,6 @@ export class RemoteMachineProvider implements MachineProvider {
86
84
  * Connect to relay server
87
85
  */
88
86
  async connect(): Promise<void> {
89
- // Dynamic import for ws module (Node.js)
90
- const { default: WebSocket } = await import('ws');
91
-
92
87
  return new Promise((resolve, reject) => {
93
88
  const url = new URL(this.config.relayUrl);
94
89
  url.searchParams.set('role', 'client');
@@ -7,7 +7,12 @@
7
7
  import { useCallback } from 'react';
8
8
  import { spawn } from 'child_process';
9
9
  import { useInbox, type UseInboxProps, type UseInboxReturn } from '../../shared/components/Inbox.js';
10
- import type { InboxItem } from '../../lib/tmux-lite/cli.js';
10
+ import {
11
+ clearInbox,
12
+ markInboxRead,
13
+ listSessions,
14
+ type InboxItem,
15
+ } from '../../lib/tmux-lite/cli.js';
11
16
 
12
17
  interface UseTUIInboxOptions {
13
18
  items: InboxItem[];
@@ -38,25 +43,21 @@ export function useTUIInbox(options: UseTUIInboxOptions): UseTUIInboxReturn {
38
43
 
39
44
  // Clear a single inbox item
40
45
  const onClearItem = useCallback(async (itemId: string) => {
41
- const { clearInbox } = await import('../../lib/tmux-lite/cli.js');
42
46
  await clearInbox(itemId);
43
47
  }, []);
44
48
 
45
49
  // Clear all inbox items
46
50
  const onClearAll = useCallback(async () => {
47
- const { clearInbox } = await import('../../lib/tmux-lite/cli.js');
48
51
  await clearInbox();
49
52
  }, []);
50
53
 
51
54
  // Mark an item as read
52
55
  const onMarkRead = useCallback(async (itemId: string) => {
53
- const { markInboxRead } = await import('../../lib/tmux-lite/cli.js');
54
56
  await markInboxRead(itemId);
55
57
  }, []);
56
58
 
57
59
  // Attach to a session
58
60
  const onAttachSession = useCallback(async (sessionId: string) => {
59
- const { listSessions } = await import('../../lib/tmux-lite/cli.js');
60
61
  const sessions = await listSessions();
61
62
  const session = sessions.find(s => s.id === sessionId);
62
63
 
@@ -6,8 +6,10 @@
6
6
  */
7
7
 
8
8
  import { useState, useCallback, useEffect, useRef } from 'react';
9
+ import WebSocket from 'ws';
9
10
  import type { MachineInfo } from '../../shared/components/index.js';
10
11
  import type { MachineProvider } from '../../shared/providers/index.js';
12
+ import { getLocalMachineProvider, createRemoteMachineProvider } from '../../shared/providers/index.js';
11
13
  import type WS from 'ws';
12
14
 
13
15
  export interface RelayConfig {
@@ -78,7 +80,6 @@ export function useRemoteMachines(options: UseRemoteMachinesOptions = {}): UseRe
78
80
  setError(null);
79
81
 
80
82
  try {
81
- const { default: WebSocket } = await import('ws');
82
83
  const socket = new WebSocket(relayConfig.url);
83
84
 
84
85
  socket.on('open', () => {
@@ -147,7 +148,6 @@ export function useRemoteMachines(options: UseRemoteMachinesOptions = {}): UseRe
147
148
 
148
149
  if (isLocal(machine)) {
149
150
  // Return local provider
150
- const { getLocalMachineProvider } = await import('../../shared/providers/index.js');
151
151
  return getLocalMachineProvider();
152
152
  }
153
153
 
@@ -157,7 +157,6 @@ export function useRemoteMachines(options: UseRemoteMachinesOptions = {}): UseRe
157
157
  return null;
158
158
  }
159
159
 
160
- const { createRemoteMachineProvider } = await import('../../shared/providers/index.js');
161
160
  return createRemoteMachineProvider({
162
161
  relayUrl: relayConfig.url,
163
162
  machineId: machine.machineId,
Binary file
@@ -1,20 +0,0 @@
1
- {
2
- "name": "@gitspace/darwin-arm64",
3
- "version": "0.2.0-rc.1",
4
- "description": "GitSpace CLI binary for darwin-arm64",
5
- "os": [
6
- "darwin"
7
- ],
8
- "cpu": [
9
- "arm64"
10
- ],
11
- "bin": {
12
- "gssh-darwin-arm64": "bin/gssh"
13
- },
14
- "repository": {
15
- "type": "git",
16
- "url": "https://github.com/inkibra/gitspace.sh.git"
17
- },
18
- "homepage": "https://gitspace.sh",
19
- "license": "SEE LICENSE IN LICENSE"
20
- }