gitspace 0.2.0-rc.2 → 0.2.0-rc.4

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 (31) hide show
  1. package/.claude/settings.local.json +5 -1
  2. package/AGENTS.md +28 -16
  3. package/README.md +11 -2
  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/src/commands/auth.ts +5 -0
  12. package/src/commands/host.ts +77 -5
  13. package/src/commands/serve.ts +60 -17
  14. package/src/commands/share.ts +0 -5
  15. package/src/core/bundle.ts +2 -2
  16. package/src/index.ts +29 -1
  17. package/src/lib/tmux-lite/cli.ts +37 -9
  18. package/src/lib/tmux-lite/server-lifecycle.test.ts +208 -0
  19. package/src/lib/tmux-lite/server.ts +5 -1
  20. package/src/relay/server.ts +6 -4
  21. package/src/shared/providers/RemoteMachineProvider.ts +1 -6
  22. package/src/tui/hooks/useInboxTUI.ts +6 -5
  23. package/src/tui/hooks/useRemoteMachines.ts +2 -3
  24. package/npm/darwin-arm64/bin/gssh +0 -0
  25. package/npm/darwin-arm64/package.json +0 -20
  26. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  27. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  28. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  29. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  30. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  31. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
@@ -15,7 +15,11 @@
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:*)",
21
+ "Bash(git add:*)",
22
+ "Bash(git commit:*)"
19
23
  ]
20
24
  }
21
25
  }
package/AGENTS.md CHANGED
@@ -215,10 +215,11 @@ src/
215
215
  | Command | Description |
216
216
  |---------|-------------|
217
217
  | `gssh relay start` | Start relay server |
218
- | `gssh relay token` | Create account JWT (HMAC) |
219
- | `gssh relay authorize <pubkey>` | Authorize a client on relay |
220
- | `gssh relay revoke <id>` | Revoke client authorization |
218
+ | `gssh relay authorize <pubkey>` | Authorize a machine on relay |
219
+ | `gssh relay revoke <id>` | Revoke machine authorization |
221
220
  | `gssh relay machines` | List authorized machines |
221
+ | `gssh relay trusted` | List trusted relays |
222
+ | `gssh relay untrust <url>` | Remove relay from trusted list |
222
223
 
223
224
  ### tmux-lite Daemon
224
225
  | Command | Description |
@@ -263,10 +264,19 @@ src/
263
264
  └─────────────────┘ └─────────────────┘ └─────────────────┘
264
265
  ```
265
266
 
266
- ### Connection Flow
267
+ ### Connection Flow (gitspace.sh Hosting)
267
268
 
268
- 1. Machine runs `gssh serve --relay <url> --token <jwt>`
269
- 2. Machine registers with relay (sends public keys)
269
+ 1. User logs in: `gssh auth login` (GitHub OAuth)
270
+ 2. User reserves subdomain: `gssh host reserve <name>`
271
+ 3. Machine runs `gssh serve start` (auto-starts local relay + cloudflared tunnel)
272
+ 4. Client connects via web: `https://<name>.gitspace.sh`
273
+ 5. X3DH handshake establishes session keys
274
+ 6. All terminal I/O is E2E encrypted
275
+
276
+ ### Connection Flow (Self-Hosted Relay)
277
+
278
+ 1. Machine runs `gssh serve start --relay ws://relay:4480/ws`
279
+ 2. Machine registers with relay (Ed25519 challenge-response auth)
270
280
  3. Machine creates invite: `gssh share create`
271
281
  4. Client connects with invite: `gssh connect <token>`
272
282
  5. X3DH handshake establishes session keys
@@ -280,7 +290,7 @@ src/
280
290
  | Key exchange | X25519 |
281
291
  | Symmetric encryption | AES-256-GCM |
282
292
  | Key derivation | HKDF-SHA256 |
283
- | JWT signing | HMAC-SHA256 |
293
+ | Relay authentication | Ed25519 challenge-response |
284
294
 
285
295
  ## TUI/Web Shared Component Pattern
286
296
 
@@ -322,11 +332,14 @@ bun run build
322
332
  # Run TUI
323
333
  bun src/index.ts
324
334
 
325
- # Run relay
326
- RELAY_SIGNING_SECRET=secret bun src/index.ts relay start
335
+ # Run relay (uses Ed25519 identity from keychain)
336
+ bun src/index.ts relay start
337
+
338
+ # Run serve with gitspace.sh hosting (requires auth login + host reserve)
339
+ bun src/index.ts serve start
327
340
 
328
- # Run serve
329
- bun src/index.ts serve --relay ws://localhost:8080/ws --token <jwt>
341
+ # Run serve with self-hosted relay
342
+ bun src/index.ts serve start --relay ws://localhost:4480/ws
330
343
  ```
331
344
 
332
345
  ### Code Style
@@ -409,6 +422,7 @@ bun src/index.ts serve --relay ws://localhost:8080/ws --token <jwt>
409
422
  | Access list | `~/gitspace/.access.json` | Authorized client public keys |
410
423
  | Machine info | `~/gitspace/.machine.json` | Machine registration |
411
424
  | Relay config | `~/gitspace/.relay.json` | Relay configuration cache |
425
+ | Host config | `~/gitspace/host.json` | gitspace.sh subdomain config |
412
426
  | Daemon state | `~/gitspace/.serve/` | PID files, status sockets |
413
427
  | tmux-lite state | `/tmp/` | Session data (configurable via `TMUX_LITE_SESSION_DIR`) |
414
428
 
@@ -418,9 +432,8 @@ bun src/index.ts serve --relay ws://localhost:8080/ws --token <jwt>
418
432
  |----------|-------------|---------|
419
433
  | `RELAY_PORT` | Relay server port | `4480` |
420
434
  | `RELAY_BIND` | Relay bind address | `0.0.0.0` |
421
- | `RELAY_SIGNING_SECRET` | Secret for HMAC JWT signing | Required |
422
435
  | `RELAY_PRIVATE_KEY` | Base64 Ed25519 private key | Uses keychain |
423
- | `SPACES_CURRENT_PROJECT` | Override current project | From config |
436
+ | `RELAY_LABEL` | Label for relay identity | None |
424
437
  | `GITSPACE_API_URL` | gitspace.sh API URL | `https://api.gitspace.sh` |
425
438
 
426
439
  ### Default Values
@@ -431,9 +444,8 @@ bun src/index.ts serve --relay ws://localhost:8080/ws --token <jwt>
431
444
  | Base branch | `main` | Global/project config |
432
445
  | Stale workspace days | `30` | Global config |
433
446
  | Relay port | `4480` | CLI/env |
434
- | Default relay URL | `wss://relay.gitspace.sh` | CLI |
435
447
 
436
448
  ---
437
449
 
438
- **Last Updated**: 2025-01
439
- **Runtime**: Bun / Node.js 18+
450
+ **Last Updated**: 2026-01
451
+ **Runtime**: Bun 1.3+
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
@@ -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.2",
3
+ "version": "0.2.0-rc.4",
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.2",
21
- "@gitspace/darwin-x64": "0.2.0-rc.2",
22
- "@gitspace/linux-x64": "0.2.0-rc.2",
23
- "@gitspace/linux-arm64": "0.2.0-rc.2"
20
+ "@gitspace/darwin-arm64": "0.2.0-rc.4",
21
+ "@gitspace/darwin-x64": "0.2.0-rc.4",
22
+ "@gitspace/linux-x64": "0.2.0-rc.4",
23
+ "@gitspace/linux-arm64": "0.2.0-rc.4"
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
  }
@@ -12,6 +12,7 @@ import { sign, serializeIdentity } from '../lib/tmux-lite/crypto/identity.js';
12
12
  import { promptPassword } from '../utils/prompts.js';
13
13
  import { logger } from '../utils/logger.js';
14
14
  import { NoIdentityError, SpacesError } from '../types/errors.js';
15
+ import { syncHostConfig } from './host.js';
15
16
 
16
17
  // API Configuration
17
18
  const API_BASE = process.env.GITSPACE_API_URL || 'https://api.gitspace.sh';
@@ -194,6 +195,10 @@ export async function authLogin(): Promise<void> {
194
195
  logger.success('Authentication complete');
195
196
  logger.success(`Logged in as ${user.github_username}`);
196
197
  logger.success('Token saved to keychain');
198
+
199
+ // Step 6: Sync host config (fetches existing subdomains from API)
200
+ // Interactive mode will prompt user to select primary or reserve a subdomain
201
+ await syncHostConfig(true);
197
202
  }
198
203
 
199
204
  // ============================================================================
@@ -85,9 +85,11 @@ function writeHostConfig(config: HostConfig): void {
85
85
  }
86
86
 
87
87
  /**
88
- * Update host config after subdomain changes
88
+ * Sync host config from gitspace.sh API
89
+ * Called after login and subdomain changes to keep local config in sync
90
+ * @param interactive - If true, prompt user to select primary if needed
89
91
  */
90
- async function syncHostConfig(): Promise<void> {
92
+ export async function syncHostConfig(interactive: boolean = false): Promise<void> {
91
93
  const token = await getSecret('GITSPACE_TOKEN');
92
94
  if (!token) return;
93
95
 
@@ -99,7 +101,40 @@ async function syncHostConfig(): Promise<void> {
99
101
 
100
102
  const subdomains: SubdomainInfo[] = await res.json();
101
103
  const activeSubdomains = subdomains.filter((s) => s.status === 'active');
102
- const primary = activeSubdomains.find((s) => s.is_primary);
104
+
105
+ // No subdomains - tell user to reserve one
106
+ if (activeSubdomains.length === 0) {
107
+ if (interactive) {
108
+ logger.log('');
109
+ logger.dim('No subdomains reserved yet.');
110
+ logger.dim('To enable remote access, reserve a subdomain:');
111
+ logger.command(' gssh host reserve <name>');
112
+ }
113
+ return;
114
+ }
115
+
116
+ // Check for primary
117
+ let primary = activeSubdomains.find((s) => s.is_primary);
118
+
119
+ // If no primary set and interactive, ask user to pick one
120
+ if (!primary && interactive && activeSubdomains.length > 0) {
121
+ logger.log('');
122
+ logger.log('Your subdomains:');
123
+ activeSubdomains.forEach((s, i) => {
124
+ logger.log(` ${i + 1}. ${s.subdomain}.gitspace.sh`);
125
+ });
126
+
127
+ if (activeSubdomains.length === 1) {
128
+ // Auto-set the only one as primary
129
+ primary = activeSubdomains[0];
130
+ logger.dim(`Setting ${primary.subdomain}.gitspace.sh as primary...`);
131
+ await hostSetPrimary(primary.subdomain);
132
+ } else {
133
+ logger.log('');
134
+ logger.dim('Select a primary subdomain for this machine:');
135
+ logger.command(' gssh host set-primary <name>');
136
+ }
137
+ }
103
138
 
104
139
  if (primary) {
105
140
  writeHostConfig({
@@ -107,6 +142,26 @@ async function syncHostConfig(): Promise<void> {
107
142
  subdomains: activeSubdomains.map((s) => s.subdomain),
108
143
  createdAt: primary.created_at,
109
144
  });
145
+
146
+ // Sync tunnel token if not present (e.g., new machine with existing account)
147
+ const existingToken = await getSecret(`TUNNEL_TOKEN_${primary.subdomain}`);
148
+ if (!existingToken) {
149
+ if (interactive) {
150
+ logger.dim(`Fetching tunnel credentials for ${primary.subdomain}.gitspace.sh...`);
151
+ }
152
+ try {
153
+ const tokenRes = await fetch(`${API_BASE}/subdomains/${primary.subdomain}/token`, { headers });
154
+ if (tokenRes.ok) {
155
+ const { tunnelToken } = await tokenRes.json();
156
+ await setSecret(`TUNNEL_TOKEN_${primary.subdomain}`, tunnelToken);
157
+ if (interactive) {
158
+ logger.success('Tunnel credentials saved');
159
+ }
160
+ }
161
+ } catch {
162
+ // Ignore token fetch errors
163
+ }
164
+ }
110
165
  }
111
166
  } catch {
112
167
  // Ignore sync errors
@@ -378,11 +433,28 @@ export async function hostStatus(): Promise<void> {
378
433
  logger.log(`Status: ${primary.status}`);
379
434
 
380
435
  // Check if tunnel token exists locally
381
- const tunnelToken = await getSecret(`TUNNEL_TOKEN_${primary.subdomain}`);
436
+ let tunnelToken = await getSecret(`TUNNEL_TOKEN_${primary.subdomain}`);
437
+
438
+ // Auto-fetch token if missing
439
+ if (!tunnelToken) {
440
+ logger.dim('Tunnel credentials missing, fetching...');
441
+ try {
442
+ const tokenRes = await fetch(`${API_BASE}/subdomains/${primary.subdomain}/token`, { headers });
443
+ if (tokenRes.ok) {
444
+ const { tunnelToken: newToken } = await tokenRes.json();
445
+ await setSecret(`TUNNEL_TOKEN_${primary.subdomain}`, newToken);
446
+ tunnelToken = newToken;
447
+ logger.success('Tunnel credentials synced');
448
+ }
449
+ } catch {
450
+ // Ignore
451
+ }
452
+ }
453
+
382
454
  logger.log(`Tunnel token: ${tunnelToken ? 'configured' : 'missing'}`);
383
455
 
384
456
  if (!tunnelToken) {
385
- logger.dim('Run: gssh host reserve ' + primary.subdomain + ' (to refresh token)');
457
+ logger.warning('Could not fetch tunnel credentials');
386
458
  }
387
459
  } catch {
388
460
  logger.log('Could not verify status (API unreachable)');
@@ -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
 
@@ -65,7 +66,7 @@ import {
65
66
  const PACKAGE_VERSION = '1.0.0';
66
67
 
67
68
  /** Default relay URL */
68
- const DEFAULT_RELAY_URL = 'wss://relay.gitspace.sh';
69
+ // No default relay - must use hosting or explicit --relay
69
70
 
70
71
  /** Local relay port for gitspace.sh hosting */
71
72
  const LOCAL_RELAY_PORT = 4480;
@@ -502,21 +503,36 @@ export async function serve(options: {
502
503
  const entries = readAccessList();
503
504
  accessList.import(entries);
504
505
 
505
- // Step 3: Display info
506
+ // Step 3: Check for gitspace.sh hosting or explicit relay
507
+ const hostConfig = readHostConfig();
508
+ const relayUrl = options.relay; // No default - must use hosting or explicit --relay
509
+
510
+ // If no hosting config and no explicit relay, error out
511
+ if (!hostConfig?.subdomain && !relayUrl) {
512
+ throw new SpacesError(
513
+ 'No relay configured.\n\n' +
514
+ 'Either set up gitspace.sh hosting:\n' +
515
+ ' gssh auth login\n' +
516
+ ' gssh host reserve <subdomain>\n\n' +
517
+ 'Or specify a relay explicitly:\n' +
518
+ ' gssh serve start --relay ws://localhost:4480/ws',
519
+ 'USER_ERROR'
520
+ );
521
+ }
522
+
523
+ // Display info
506
524
  const machineIdentity = readMachineIdentity();
507
525
  const machineId = machineIdentity?.machineId ?? identity.id;
508
- const relayUrl = options.relay ?? DEFAULT_RELAY_URL;
509
526
 
510
527
  logger.log('');
511
528
  logger.bold('Machine Identity:');
512
529
  logger.log(` ID: ${machineId}`);
513
- logger.log(` Relay: ${relayUrl}`);
530
+ if (relayUrl) {
531
+ logger.log(` Relay: ${relayUrl}`);
532
+ }
514
533
  logger.log('');
515
534
  logger.dim(`Access list: ${entries.length} authorized ${entries.length === 1 ? 'client' : 'clients'}`);
516
535
  logger.log('');
517
-
518
- // Step 3b: Check for gitspace.sh hosting
519
- const hostConfig = readHostConfig();
520
536
  let localRelayServer: ReturnType<typeof createRelayServer> | null = null;
521
537
  let localRelayIdentity: ReturnType<typeof generateRelayIdentity> | null = null;
522
538
 
@@ -607,6 +623,11 @@ export async function serve(options: {
607
623
  return;
608
624
  }
609
625
 
626
+ // If we get here, relayUrl must be defined (checked earlier)
627
+ if (!relayUrl) {
628
+ throw new SpacesError('No relay URL configured', 'USER_ERROR');
629
+ }
630
+
610
631
  // Step 4: Create session manager
611
632
  const sessionManager = new ClientSessionManager({
612
633
  relay: relayUrl,
@@ -1207,21 +1228,28 @@ export async function serveStart(options: {
1207
1228
  logger.log('Starting serve daemon...');
1208
1229
 
1209
1230
  // Build args for background process
1210
- const args = [process.argv[1], 'serve', 'start', '--foreground'];
1211
- if (options.relay) args.push('--relay', options.relay);
1212
- if (options.relayPubkey) args.push('--relay-pubkey', options.relayPubkey);
1213
- args.push('--password-stdin');
1231
+ // Detect if we're running as a compiled binary vs dev mode
1232
+ const isCompiled = !process.execPath.endsWith('bun');
1233
+
1234
+ const serveArgs = ['serve', 'start', '--foreground'];
1235
+ if (options.relay) serveArgs.push('--relay', options.relay);
1236
+ if (options.relayPubkey) serveArgs.push('--relay-pubkey', options.relayPubkey);
1237
+ serveArgs.push('--password-stdin');
1238
+
1239
+ // Build command: compiled binary runs directly, dev mode uses bun
1240
+ const cmd = isCompiled
1241
+ ? [process.execPath, ...serveArgs]
1242
+ : ['bun', process.argv[1], ...serveArgs];
1214
1243
 
1215
1244
  // Write output to log file for debugging
1216
1245
  const logFile = getServeLogFile();
1217
- const { ensureServeDaemonDir } = await import('../serve/daemon.js');
1218
1246
  ensureServeDaemonDir();
1219
1247
 
1220
1248
  // Truncate log file at start
1221
1249
  await Bun.write(logFile, `[${new Date().toISOString()}] Starting serve daemon...\n`);
1222
1250
 
1223
1251
  const child = spawn({
1224
- cmd: ['bun', ...args],
1252
+ cmd,
1225
1253
  stdin: 'pipe',
1226
1254
  stdout: Bun.file(logFile),
1227
1255
  stderr: Bun.file(logFile),
@@ -1269,20 +1297,35 @@ export async function serveStart(options: {
1269
1297
  // Get config
1270
1298
  const machineIdentity = readMachineIdentity();
1271
1299
  const machineId = machineIdentity?.machineId ?? identity.id;
1272
- const relayUrl = options.relay ?? DEFAULT_RELAY_URL;
1273
1300
 
1274
1301
  // Check for gitspace.sh hosting
1275
1302
  const hostConfig = readHostConfig();
1303
+ const relayUrl = options.relay; // No default - must use hosting or explicit --relay
1304
+
1305
+ // If no hosting config and no explicit relay, error out
1306
+ if (!hostConfig?.subdomain && !relayUrl) {
1307
+ cleanupServeFiles();
1308
+ throw new SpacesError(
1309
+ 'No relay configured.\n\n' +
1310
+ 'Either set up gitspace.sh hosting:\n' +
1311
+ ' gssh auth login\n' +
1312
+ ' gssh host reserve <subdomain>\n\n' +
1313
+ 'Or specify a relay explicitly:\n' +
1314
+ ' gssh serve start --relay ws://localhost:4480/ws',
1315
+ 'USER_ERROR'
1316
+ );
1317
+ }
1318
+
1276
1319
  let localRelayServer: ReturnType<typeof createRelayServer> | null = null;
1277
1320
  let localRelayIdentity: ReturnType<typeof generateRelayIdentity> | null = null;
1278
- let effectiveRelayUrl = relayUrl;
1321
+ let effectiveRelayUrl = relayUrl || '';
1279
1322
 
1280
1323
  // Initialize daemon state
1281
1324
  setDaemonState({
1282
1325
  version: PACKAGE_VERSION,
1283
1326
  startTime: Date.now(),
1284
1327
  relay: {
1285
- url: relayUrl,
1328
+ url: effectiveRelayUrl,
1286
1329
  status: 'connecting',
1287
1330
  },
1288
1331
  clients: 0,
@@ -1373,7 +1416,7 @@ export async function serveStart(options: {
1373
1416
 
1374
1417
  // Save relay config for share/access commands
1375
1418
  writeRelayConfig({
1376
- relayUrl,
1419
+ relayUrl: effectiveRelayUrl,
1377
1420
  machineId,
1378
1421
  savedAt: Date.now(),
1379
1422
  });
@@ -27,11 +27,6 @@ import { SpacesError, NoIdentityError, InvalidPasswordError } from '../types/err
27
27
  import chalk from 'chalk';
28
28
  import { createHash } from 'crypto';
29
29
 
30
- /**
31
- * Default relay URL
32
- */
33
- const DEFAULT_RELAY_URL = 'wss://relay.gitspace.sh';
34
-
35
30
  /**
36
31
  * Duration string formats supported:
37
32
  * - "1h" -> 1 hour
@@ -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
@@ -5,9 +5,37 @@
5
5
  * Manages GitHub workspaces with git worktrees and secure remote terminal access
6
6
  */
7
7
 
8
+ // Internal command: run tmux-lite server directly (for compiled binary)
9
+ // This must be checked before any other imports to avoid loading unnecessary modules
10
+ if (process.argv.includes('--internal-tmux-server')) {
11
+ // Pass through --test flag if present
12
+ if (process.argv.includes('--test')) {
13
+ process.env.TMUX_LITE_SOCKET = '/tmp/tmux-lite-test.sock';
14
+ process.env.TMUX_LITE_SESSION_DIR = '/tmp/tmux-lite-test';
15
+ process.env.TMUX_LITE_PID_FILE = '/tmp/tmux-lite-test.pid';
16
+ }
17
+ // Import and run server (module auto-starts on import)
18
+ await import('./lib/tmux-lite/server.js');
19
+ // Keep process alive - server runs via Bun.listen() which is async
20
+ // We need to prevent the rest of this file from executing
21
+ await new Promise(() => {}); // Block forever
22
+ }
23
+
8
24
  import { Command } from 'commander'
25
+ import { readFileSync } from 'fs'
26
+ import { join } from 'path'
9
27
  import { isFirstTimeSetup, initializeSpaces } from './core/config.js'
10
- import { VERSION } from './version.generated.js'
28
+ import { VERSION as GENERATED_VERSION } from './version.generated.js'
29
+
30
+ // Read version from package.json in dev, fall back to generated for compiled binary
31
+ let VERSION = GENERATED_VERSION
32
+ try {
33
+ const pkgPath = join(import.meta.dir, '../package.json')
34
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
35
+ VERSION = pkg.version
36
+ } catch {
37
+ // Compiled binary - use generated version
38
+ }
11
39
  import { logger } from './utils/logger.js'
12
40
  import { SpacesError } from './types/errors.js'
13
41
  import { addProject, addWorkspace } from './commands/add.js'
@@ -67,9 +67,22 @@ if (isTestMode) {
67
67
  process.env.TMUX_LITE_SESSION_DIR = "/tmp/tmux-lite-test";
68
68
  }
69
69
 
70
- const getServerCommand = (): string[] => (
71
- isTestMode ? ["bun", "run", SERVER_SCRIPT, "--test"] : ["bun", "run", SERVER_SCRIPT]
72
- );
70
+ const getServerCommand = (): string[] => {
71
+ // Detect if we're running as a compiled binary (not bun)
72
+ const isCompiled = !process.execPath.endsWith('bun');
73
+
74
+ if (isCompiled) {
75
+ // Use the binary with internal flag
76
+ return isTestMode
77
+ ? [process.execPath, '--internal-tmux-server', '--test']
78
+ : [process.execPath, '--internal-tmux-server'];
79
+ }
80
+
81
+ // Dev mode: use bun run
82
+ return isTestMode
83
+ ? ['bun', 'run', SERVER_SCRIPT, '--test']
84
+ : ['bun', 'run', SERVER_SCRIPT];
85
+ };
73
86
 
74
87
  // Check if we're already inside a tmux-lite session
75
88
  export function isNested(): boolean {
@@ -605,8 +618,10 @@ async function main() {
605
618
  console.log("Starting server...");
606
619
  spawn({
607
620
  cmd: getServerCommand(),
608
- stdout: "inherit",
609
- stderr: "inherit",
621
+ // Use "ignore" so server doesn't inherit CLI's stdout/stderr
622
+ // This allows CLI to exit cleanly when piped
623
+ stdout: "ignore",
624
+ stderr: "ignore",
610
625
  });
611
626
  await Bun.sleep(300);
612
627
  if (!(await isServerRunning())) {
@@ -787,10 +802,23 @@ In session:
787
802
  }
788
803
  }
789
804
 
805
+ // Commands that are non-interactive and should exit immediately after completion
806
+ const NON_INTERACTIVE_COMMANDS = new Set([
807
+ "list", "ls", "kill", "kill-server", "inbox", "inbox-clear", "status", "version"
808
+ ]);
809
+
790
810
  // Only run CLI when executed directly, not when imported as a module
791
811
  if (import.meta.main) {
792
- main().catch(e => {
793
- console.error(e.message);
794
- process.exit(1);
795
- });
812
+ main()
813
+ .then(() => {
814
+ // Force exit after non-interactive commands complete
815
+ // Some socket references may keep the event loop alive otherwise
816
+ if (NON_INTERACTIVE_COMMANDS.has(cmd)) {
817
+ process.exit(0);
818
+ }
819
+ })
820
+ .catch(e => {
821
+ console.error(e.message);
822
+ process.exit(1);
823
+ });
796
824
  }
@@ -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":
@@ -68,25 +68,27 @@ function getContentType(pathname: string): string {
68
68
  * Serve a static file - tries embedded assets first, falls back to filesystem
69
69
  */
70
70
  async function serveStaticFile(pathname: string): Promise<Response | null> {
71
+ // Normalize path for content type (/ -> /index.html)
72
+ const normalizedPath = pathname === "/" ? "/index.html" : pathname;
73
+
71
74
  // Try embedded assets first (compiled binary)
72
75
  if (hasEmbeddedAssets() && embeddedAssets) {
73
76
  const blob = embeddedAssets.getEmbeddedFile(pathname);
74
77
  if (blob) {
75
78
  return new Response(blob, {
76
- headers: { "Content-Type": getContentType(pathname) },
79
+ headers: { "Content-Type": getContentType(normalizedPath) },
77
80
  });
78
81
  }
79
82
  }
80
83
 
81
84
  // Fall back to filesystem (development mode)
82
- const filePath = pathname === "/" ? "/index.html" : pathname;
83
- const resolvedPath = resolveAssetPath(filePath);
85
+ const resolvedPath = resolveAssetPath(normalizedPath);
84
86
  if (!resolvedPath) return null;
85
87
 
86
88
  const file = Bun.file(resolvedPath);
87
89
  if (await file.exists()) {
88
90
  return new Response(file, {
89
- headers: { "Content-Type": getContentType(pathname) },
91
+ headers: { "Content-Type": getContentType(normalizedPath) },
90
92
  });
91
93
  }
92
94
 
@@ -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.2",
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
- }