@seflless/ghosttown 1.2.4 → 1.3.0
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/bin/ghosttown.js +152 -13
- package/package.json +3 -2
package/bin/ghosttown.js
CHANGED
|
@@ -3,22 +3,24 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* ghosttown CLI - Web-based terminal emulator
|
|
5
5
|
*
|
|
6
|
-
* Starts a local HTTP server with WebSocket PTY support
|
|
6
|
+
* Starts a local HTTP server with WebSocket PTY support,
|
|
7
|
+
* or runs a command in a new tmux session.
|
|
7
8
|
*
|
|
8
9
|
* Usage:
|
|
9
|
-
* ghosttown [options]
|
|
10
|
+
* ghosttown [options] [command]
|
|
10
11
|
*
|
|
11
12
|
* Options:
|
|
12
13
|
* -p, --port <port> Port to listen on (default: 8080, or PORT env var)
|
|
13
14
|
* -h, --help Show this help message
|
|
14
15
|
*
|
|
15
16
|
* Examples:
|
|
16
|
-
* ghosttown
|
|
17
|
-
* ghosttown -p 3000
|
|
18
|
-
* ghosttown
|
|
19
|
-
*
|
|
17
|
+
* ghosttown Start the web terminal server
|
|
18
|
+
* ghosttown -p 3000 Start the server on port 3000
|
|
19
|
+
* ghosttown vim Run vim in a new tmux session
|
|
20
|
+
* ghosttown "npm run dev" Run npm in a new tmux session
|
|
20
21
|
*/
|
|
21
22
|
|
|
23
|
+
import { execSync, spawn } from 'child_process';
|
|
22
24
|
import fs from 'fs';
|
|
23
25
|
import http from 'http';
|
|
24
26
|
import { homedir, networkInterfaces } from 'os';
|
|
@@ -35,6 +37,118 @@ import { asciiArt } from './ascii.js';
|
|
|
35
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
38
|
const __dirname = path.dirname(__filename);
|
|
37
39
|
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Tmux Session Management
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if tmux is installed
|
|
46
|
+
*/
|
|
47
|
+
function checkTmuxInstalled() {
|
|
48
|
+
try {
|
|
49
|
+
execSync('which tmux', { stdio: 'pipe' });
|
|
50
|
+
return true;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Print tmux installation instructions and exit
|
|
58
|
+
*/
|
|
59
|
+
function printTmuxInstallHelp() {
|
|
60
|
+
console.error('\x1b[31mError: tmux is not installed.\x1b[0m\n');
|
|
61
|
+
console.error('tmux is required to run commands in ghosttown sessions.\n');
|
|
62
|
+
console.error('To install tmux:\n');
|
|
63
|
+
|
|
64
|
+
if (process.platform === 'darwin') {
|
|
65
|
+
console.error(' \x1b[36mmacOS (Homebrew):\x1b[0m brew install tmux');
|
|
66
|
+
console.error(' \x1b[36mmacOS (MacPorts):\x1b[0m sudo port install tmux');
|
|
67
|
+
} else if (process.platform === 'linux') {
|
|
68
|
+
console.error(' \x1b[36mDebian/Ubuntu:\x1b[0m sudo apt install tmux');
|
|
69
|
+
console.error(' \x1b[36mFedora:\x1b[0m sudo dnf install tmux');
|
|
70
|
+
console.error(' \x1b[36mArch:\x1b[0m sudo pacman -S tmux');
|
|
71
|
+
} else {
|
|
72
|
+
console.error(' Please install tmux using your system package manager.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.error('');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the next available ghosttown session ID
|
|
81
|
+
* Scans existing tmux sessions named ghosttown-<N> and returns max(N) + 1
|
|
82
|
+
*/
|
|
83
|
+
function getNextSessionId() {
|
|
84
|
+
try {
|
|
85
|
+
// List all tmux sessions
|
|
86
|
+
const output = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Find all ghosttown-<N> sessions and extract IDs
|
|
92
|
+
const sessionIds = output
|
|
93
|
+
.split('\n')
|
|
94
|
+
.filter((name) => name.startsWith('ghosttown-'))
|
|
95
|
+
.map((name) => {
|
|
96
|
+
const id = parseInt(name.replace('ghosttown-', ''), 10);
|
|
97
|
+
return isNaN(id) ? 0 : id;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Return max + 1, or 1 if no sessions exist
|
|
101
|
+
return sessionIds.length > 0 ? Math.max(...sessionIds) + 1 : 1;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// tmux not running or no sessions - start with 1
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a new tmux session and run the command
|
|
110
|
+
*/
|
|
111
|
+
function createTmuxSession(command) {
|
|
112
|
+
// Check if tmux is installed
|
|
113
|
+
if (!checkTmuxInstalled()) {
|
|
114
|
+
printTmuxInstallHelp();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sessionId = getNextSessionId();
|
|
118
|
+
const sessionName = `ghosttown-${sessionId}`;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// Create tmux session and attach directly (not detached)
|
|
122
|
+
// Use -x and -y to set initial size, avoiding "terminal too small" issues
|
|
123
|
+
// The command runs in the session, and we attach immediately
|
|
124
|
+
const attach = spawn(
|
|
125
|
+
'tmux',
|
|
126
|
+
[
|
|
127
|
+
'new-session',
|
|
128
|
+
'-s',
|
|
129
|
+
sessionName,
|
|
130
|
+
'-x',
|
|
131
|
+
'200',
|
|
132
|
+
'-y',
|
|
133
|
+
'50',
|
|
134
|
+
// Set status off (no HUD), run the command, then start a shell
|
|
135
|
+
// so the session stays open and interactive after command completes
|
|
136
|
+
'tmux set-option status off; ' + command + '; exec $SHELL',
|
|
137
|
+
],
|
|
138
|
+
{
|
|
139
|
+
stdio: 'inherit',
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
attach.on('exit', (code) => {
|
|
144
|
+
process.exit(code || 0);
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(`Error creating tmux session: ${err.message}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
38
152
|
// ============================================================================
|
|
39
153
|
// Parse CLI arguments
|
|
40
154
|
// ============================================================================
|
|
@@ -42,23 +156,28 @@ const __dirname = path.dirname(__filename);
|
|
|
42
156
|
function parseArgs() {
|
|
43
157
|
const args = process.argv.slice(2);
|
|
44
158
|
let port = null;
|
|
159
|
+
let command = null;
|
|
45
160
|
|
|
46
161
|
for (let i = 0; i < args.length; i++) {
|
|
47
162
|
const arg = args[i];
|
|
48
163
|
|
|
49
164
|
if (arg === '-h' || arg === '--help') {
|
|
50
165
|
console.log(`
|
|
51
|
-
Usage: ghosttown [options]
|
|
166
|
+
Usage: ghosttown [options] [command]
|
|
52
167
|
|
|
53
168
|
Options:
|
|
54
169
|
-p, --port <port> Port to listen on (default: 8080, or PORT env var)
|
|
55
170
|
-h, --help Show this help message
|
|
56
171
|
|
|
172
|
+
Commands:
|
|
173
|
+
If a command is provided, ghosttown creates a new tmux session
|
|
174
|
+
named ghosttown-<id> (with status bar disabled) and runs the command.
|
|
175
|
+
|
|
57
176
|
Examples:
|
|
58
|
-
ghosttown
|
|
59
|
-
ghosttown -p 3000
|
|
60
|
-
ghosttown
|
|
61
|
-
|
|
177
|
+
ghosttown Start the web terminal server
|
|
178
|
+
ghosttown -p 3000 Start the server on port 3000
|
|
179
|
+
ghosttown vim Run vim in a new tmux session
|
|
180
|
+
ghosttown "npm run dev" Run npm in a new tmux session
|
|
62
181
|
`);
|
|
63
182
|
process.exit(0);
|
|
64
183
|
}
|
|
@@ -75,14 +194,33 @@ Examples:
|
|
|
75
194
|
process.exit(1);
|
|
76
195
|
}
|
|
77
196
|
i++; // Skip the next argument since we consumed it
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Any non-flag argument is treated as the command
|
|
201
|
+
if (!arg.startsWith('-')) {
|
|
202
|
+
command = arg;
|
|
78
203
|
}
|
|
79
204
|
}
|
|
80
205
|
|
|
81
|
-
return { port };
|
|
206
|
+
return { port, command };
|
|
82
207
|
}
|
|
83
208
|
|
|
84
209
|
const cliArgs = parseArgs();
|
|
85
|
-
|
|
210
|
+
|
|
211
|
+
// If a command is provided, create a tmux session instead of starting server
|
|
212
|
+
if (cliArgs.command) {
|
|
213
|
+
createTmuxSession(cliArgs.command);
|
|
214
|
+
// createTmuxSession spawns tmux attach and waits for it to exit
|
|
215
|
+
// The script will exit when tmux attach exits (via the exit handler)
|
|
216
|
+
// We must not continue to server code, so we stop here
|
|
217
|
+
} else {
|
|
218
|
+
// Server mode (no command provided)
|
|
219
|
+
startWebServer();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function startWebServer() {
|
|
223
|
+
const HTTP_PORT = cliArgs.port || process.env.PORT || 8080;
|
|
86
224
|
|
|
87
225
|
// ============================================================================
|
|
88
226
|
// Locate ghosttown assets
|
|
@@ -708,3 +846,4 @@ httpServer.listen(HTTP_PORT, '0.0.0.0', async () => {
|
|
|
708
846
|
|
|
709
847
|
printBanner(`http://localhost:${HTTP_PORT}`);
|
|
710
848
|
});
|
|
849
|
+
} // end startWebServer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seflless/ghosttown",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Web-based terminal emulator using Ghostty's VT100 parser via WebAssembly",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/ghostty-web.umd.cjs",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"lint": "biome check .",
|
|
65
65
|
"lint:fix": "biome check --write .",
|
|
66
66
|
"prepublishOnly": "bun run build",
|
|
67
|
-
"cli:publish": "node scripts/cli-publish.js"
|
|
67
|
+
"cli:publish": "node scripts/cli-publish.js",
|
|
68
|
+
"kill:8080": "kill -9 $(lsof -ti :8080)"
|
|
68
69
|
},
|
|
69
70
|
"dependencies": {
|
|
70
71
|
"@lydell/node-pty": "^1.0.1",
|