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.
- package/.claude/settings.local.json +3 -1
- package/README.md +11 -2
- package/bin/gssh +19 -3
- package/docs/QUICKSTART.md +11 -2
- package/docs/SITE_DOCS_FIGMA_MAKE.md +9 -7
- package/landing-page/src/components/docs/DocsContent.tsx +16 -10
- package/landing-page/src/components/landing/CTA.tsx +1 -1
- package/landing-page/src/components/landing/Features.tsx +2 -2
- package/landing-page/src/components/landing/UseCases.tsx +1 -1
- package/package.json +7 -6
- package/scripts/build.ts +14 -0
- package/src/commands/serve.ts +1 -1
- package/src/core/bundle.ts +2 -2
- package/src/index.ts +12 -11
- package/src/lib/tmux-lite/cli.ts +21 -6
- package/src/lib/tmux-lite/server-lifecycle.test.ts +208 -0
- package/src/lib/tmux-lite/server.ts +5 -1
- package/src/shared/providers/RemoteMachineProvider.ts +1 -6
- package/src/tui/hooks/useInboxTUI.ts +6 -5
- package/src/tui/hooks/useRemoteMachines.ts +2 -3
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +0 -20
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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">
|
|
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">$
|
|
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">$
|
|
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.
|
|
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.
|
|
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.
|
|
21
|
-
"@gitspace/darwin-x64": "0.2.0-rc.
|
|
22
|
-
"@gitspace/linux-x64": "0.2.0-rc.
|
|
23
|
-
"@gitspace/linux-arm64": "0.2.0-rc.
|
|
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
|
|
package/src/commands/serve.ts
CHANGED
|
@@ -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
|
package/src/core/bundle.ts
CHANGED
|
@@ -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(
|
|
49
|
+
.version(VERSION)
|
|
49
50
|
|
|
50
51
|
// First-time setup check
|
|
51
52
|
async function checkFirstTimeSetup(): Promise<void> {
|
package/src/lib/tmux-lite/cli.ts
CHANGED
|
@@ -605,8 +605,10 @@ async function main() {
|
|
|
605
605
|
console.log("Starting server...");
|
|
606
606
|
spawn({
|
|
607
607
|
cmd: getServerCommand(),
|
|
608
|
-
|
|
609
|
-
|
|
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()
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|