webtty 1.2.0 → 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/README.md +3 -3
- package/dist/cli/index.js +237 -61
- package/dist/cli/index.js.map +7 -6
- package/dist/client-browser.js +3 -0
- package/dist/client.css +18 -0
- package/dist/client.html +14 -0
- package/dist/server/index.js +70 -128
- package/dist/server/index.js.map +8 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
Terminal UI in the browser. Run CLI/TUI applications in a browser tab, across platforms.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx webtty
|
|
9
|
-
npx webtty ls
|
|
10
|
-
npx webtty help
|
|
8
|
+
npx webtty # start server + open a terminal in the browser
|
|
9
|
+
npx webtty ls # list sessions
|
|
10
|
+
npx webtty help # show all commands
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Debugging
|
package/dist/cli/index.js
CHANGED
|
@@ -2156,19 +2156,131 @@ var {
|
|
|
2156
2156
|
Help
|
|
2157
2157
|
} = import__.default;
|
|
2158
2158
|
|
|
2159
|
-
// src/cli/
|
|
2160
|
-
import * as
|
|
2159
|
+
// src/cli/commands.ts
|
|
2160
|
+
import * as childProcess2 from "node:child_process";
|
|
2161
|
+
import fs3 from "node:fs";
|
|
2162
|
+
import path3 from "node:path";
|
|
2163
|
+
|
|
2164
|
+
// src/config.ts
|
|
2161
2165
|
import fs from "node:fs";
|
|
2166
|
+
import os from "node:os";
|
|
2162
2167
|
import path from "node:path";
|
|
2168
|
+
function configDir() {
|
|
2169
|
+
return path.join(process.env.HOME ?? os.homedir(), ".config", "webtty");
|
|
2170
|
+
}
|
|
2171
|
+
function getConfigPath() {
|
|
2172
|
+
return path.join(configDir(), "config.json");
|
|
2173
|
+
}
|
|
2174
|
+
var DEFAULT_THEME = {
|
|
2175
|
+
background: "#000000",
|
|
2176
|
+
foreground: "#CCCCCC",
|
|
2177
|
+
cursor: "#FFFFFF",
|
|
2178
|
+
selection: "#FFFFFF",
|
|
2179
|
+
black: "#0C0C0C",
|
|
2180
|
+
red: "#C50F1F",
|
|
2181
|
+
green: "#13A10E",
|
|
2182
|
+
yellow: "#C19C00",
|
|
2183
|
+
blue: "#0037DA",
|
|
2184
|
+
purple: "#881798",
|
|
2185
|
+
cyan: "#3A96DD",
|
|
2186
|
+
white: "#CCCCCC",
|
|
2187
|
+
brightBlack: "#767676",
|
|
2188
|
+
brightRed: "#E74856",
|
|
2189
|
+
brightGreen: "#16C60C",
|
|
2190
|
+
brightYellow: "#F9F1A5",
|
|
2191
|
+
brightBlue: "#3B78FF",
|
|
2192
|
+
brightPurple: "#B4009E",
|
|
2193
|
+
brightCyan: "#61D6D6",
|
|
2194
|
+
brightWhite: "#F2F2F2"
|
|
2195
|
+
};
|
|
2196
|
+
var DEFAULT_CONFIG = {
|
|
2197
|
+
port: 2346,
|
|
2198
|
+
host: "127.0.0.1",
|
|
2199
|
+
shell: process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "/bin/bash",
|
|
2200
|
+
term: process.env.TERM ?? "xterm-256color",
|
|
2201
|
+
colorTerm: "truecolor",
|
|
2202
|
+
scrollback: 256 * 1024,
|
|
2203
|
+
cols: 80,
|
|
2204
|
+
rows: 24,
|
|
2205
|
+
fontSize: 13,
|
|
2206
|
+
fontFamily: "Menlo, Consolas, 'DejaVu Sans Mono', monospace",
|
|
2207
|
+
cursorBlink: true,
|
|
2208
|
+
copyOnSelect: true,
|
|
2209
|
+
rightClickBehavior: "default",
|
|
2210
|
+
logs: false,
|
|
2211
|
+
theme: DEFAULT_THEME
|
|
2212
|
+
};
|
|
2213
|
+
function loadConfig() {
|
|
2214
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
2215
|
+
try {
|
|
2216
|
+
saveConfig(DEFAULT_CONFIG);
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
console.warn(`webtty: failed to write default config to ${getConfigPath()}: ${err.message}`);
|
|
2219
|
+
return { ...DEFAULT_CONFIG };
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
let raw;
|
|
2223
|
+
try {
|
|
2224
|
+
raw = fs.readFileSync(getConfigPath(), "utf8");
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
throw new Error(`webtty: failed to read config at ${getConfigPath()}: ${err.message}`);
|
|
2227
|
+
}
|
|
2228
|
+
let parsed;
|
|
2229
|
+
try {
|
|
2230
|
+
parsed = JSON.parse(raw);
|
|
2231
|
+
} catch {
|
|
2232
|
+
throw new Error(`webtty: invalid JSON in config file ${getConfigPath()}`);
|
|
2233
|
+
}
|
|
2234
|
+
const p = parsed;
|
|
2235
|
+
return {
|
|
2236
|
+
...DEFAULT_CONFIG,
|
|
2237
|
+
...typeof p.port === "number" && { port: p.port },
|
|
2238
|
+
...typeof p.host === "string" && { host: p.host },
|
|
2239
|
+
...typeof p.shell === "string" && { shell: p.shell },
|
|
2240
|
+
...typeof p.term === "string" && { term: p.term },
|
|
2241
|
+
...typeof p.colorTerm === "string" && { colorTerm: p.colorTerm },
|
|
2242
|
+
...typeof p.scrollback === "number" && { scrollback: p.scrollback },
|
|
2243
|
+
...typeof p.cols === "number" && { cols: p.cols },
|
|
2244
|
+
...typeof p.rows === "number" && { rows: p.rows },
|
|
2245
|
+
...typeof p.fontSize === "number" && { fontSize: p.fontSize },
|
|
2246
|
+
...typeof p.fontFamily === "string" && { fontFamily: p.fontFamily },
|
|
2247
|
+
...typeof p.cursorBlink === "boolean" && { cursorBlink: p.cursorBlink },
|
|
2248
|
+
...typeof p.copyOnSelect === "boolean" && { copyOnSelect: p.copyOnSelect },
|
|
2249
|
+
...typeof p.rightClickBehavior === "string" && {
|
|
2250
|
+
rightClickBehavior: p.rightClickBehavior === "copyPaste" ? "copyPaste" : "default"
|
|
2251
|
+
},
|
|
2252
|
+
...typeof p.logs === "boolean" && { logs: p.logs },
|
|
2253
|
+
...p.theme && typeof p.theme === "object" && { theme: { ...DEFAULT_THEME, ...p.theme } }
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
function saveConfig(_config) {
|
|
2257
|
+
fs.mkdirSync(path.dirname(getConfigPath()), { recursive: true });
|
|
2258
|
+
const content = JSON.stringify({
|
|
2259
|
+
port: DEFAULT_CONFIG.port,
|
|
2260
|
+
host: DEFAULT_CONFIG.host
|
|
2261
|
+
}, null, 2);
|
|
2262
|
+
fs.writeFileSync(getConfigPath(), content, "utf8");
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/cli/http.ts
|
|
2266
|
+
import * as childProcess from "node:child_process";
|
|
2267
|
+
import fs2 from "node:fs";
|
|
2268
|
+
import path2 from "node:path";
|
|
2163
2269
|
import { fileURLToPath } from "node:url";
|
|
2164
2270
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2165
|
-
var __dirname2 =
|
|
2271
|
+
var __dirname2 = path2.dirname(__filename2);
|
|
2166
2272
|
var PORT = Number(process.env.PORT) || 2346;
|
|
2167
2273
|
var BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
2274
|
+
function logPath() {
|
|
2275
|
+
return path2.join(configDir(), "server.log");
|
|
2276
|
+
}
|
|
2168
2277
|
async function isServerRunning() {
|
|
2169
2278
|
try {
|
|
2170
|
-
await fetch(`${BASE_URL}/api/sessions`);
|
|
2171
|
-
|
|
2279
|
+
const res = await fetch(`${BASE_URL}/api/sessions`);
|
|
2280
|
+
if (!res.ok)
|
|
2281
|
+
return false;
|
|
2282
|
+
const body = await res.json();
|
|
2283
|
+
return Array.isArray(body);
|
|
2172
2284
|
} catch {
|
|
2173
2285
|
return false;
|
|
2174
2286
|
}
|
|
@@ -2176,17 +2288,28 @@ async function isServerRunning() {
|
|
|
2176
2288
|
async function startServer(timeoutMs = 1e4, _spawn = childProcess.spawn) {
|
|
2177
2289
|
const isBun = typeof globalThis.Bun !== "undefined";
|
|
2178
2290
|
const isTs = isBun && __filename2.endsWith(".ts");
|
|
2179
|
-
const serverEntry =
|
|
2180
|
-
if (!
|
|
2291
|
+
const serverEntry = path2.resolve(__dirname2, isTs ? "../server/index.ts" : "../server/index.js");
|
|
2292
|
+
if (!fs2.existsSync(serverEntry)) {
|
|
2181
2293
|
console.error(`webtty: server entry not found at ${serverEntry}`);
|
|
2182
2294
|
process.exit(1);
|
|
2183
2295
|
}
|
|
2296
|
+
const config = loadConfig();
|
|
2297
|
+
let stdio = "ignore";
|
|
2298
|
+
let logFd;
|
|
2299
|
+
if (config.logs) {
|
|
2300
|
+
const log = logPath();
|
|
2301
|
+
fs2.mkdirSync(path2.dirname(log), { recursive: true });
|
|
2302
|
+
logFd = fs2.openSync(log, "a");
|
|
2303
|
+
stdio = ["ignore", logFd, logFd];
|
|
2304
|
+
}
|
|
2184
2305
|
const child = _spawn(process.execPath, [serverEntry], {
|
|
2185
2306
|
detached: true,
|
|
2186
|
-
stdio
|
|
2307
|
+
stdio,
|
|
2187
2308
|
env: { ...process.env, PORT: String(PORT) }
|
|
2188
2309
|
});
|
|
2189
2310
|
child.unref();
|
|
2311
|
+
if (logFd !== undefined)
|
|
2312
|
+
fs2.closeSync(logFd);
|
|
2190
2313
|
const deadline = Date.now() + timeoutMs;
|
|
2191
2314
|
while (Date.now() < deadline) {
|
|
2192
2315
|
if (await isServerRunning())
|
|
@@ -2227,47 +2350,7 @@ function openBrowser(url, _spawn = childProcess.spawn) {
|
|
|
2227
2350
|
|
|
2228
2351
|
// src/cli/commands.ts
|
|
2229
2352
|
function registerCommands(program2) {
|
|
2230
|
-
program2.command("
|
|
2231
|
-
if (await isServerRunning()) {
|
|
2232
|
-
console.log("webtty is already running");
|
|
2233
|
-
return;
|
|
2234
|
-
}
|
|
2235
|
-
await startServer();
|
|
2236
|
-
console.log("webtty started");
|
|
2237
|
-
});
|
|
2238
|
-
program2.command("stop").description("Stop the webtty server").action(async () => {
|
|
2239
|
-
if (!await isServerRunning()) {
|
|
2240
|
-
console.log("webtty is not running");
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
const ok = await stopServer();
|
|
2244
|
-
if (ok) {
|
|
2245
|
-
console.log("webtty stopped");
|
|
2246
|
-
} else {
|
|
2247
|
-
console.error("webtty stop failed");
|
|
2248
|
-
process.exit(1);
|
|
2249
|
-
}
|
|
2250
|
-
});
|
|
2251
|
-
program2.command("ls").description("List all sessions").action(async () => {
|
|
2252
|
-
let res;
|
|
2253
|
-
try {
|
|
2254
|
-
res = await fetch(`${BASE_URL}/api/sessions`);
|
|
2255
|
-
} catch {
|
|
2256
|
-
console.log("webtty is not running");
|
|
2257
|
-
process.exit(1);
|
|
2258
|
-
}
|
|
2259
|
-
const sessions = await res.json();
|
|
2260
|
-
if (sessions.length === 0) {
|
|
2261
|
-
console.log("no sessions");
|
|
2262
|
-
return;
|
|
2263
|
-
}
|
|
2264
|
-
console.log("id\t\t\tconnected\tcreated");
|
|
2265
|
-
for (const s of sessions) {
|
|
2266
|
-
const created = new Date(s.createdAt).toLocaleString();
|
|
2267
|
-
console.log(`${s.id} ${s.connected} ${created}`);
|
|
2268
|
-
}
|
|
2269
|
-
});
|
|
2270
|
-
program2.command("run [id]").description("Create or reuse a session and open it in the browser").action(async (id) => {
|
|
2353
|
+
program2.command("at [id]").alias("a").alias("attach").description("Attach to a new or existing session and open it").action(async (id) => {
|
|
2271
2354
|
if (!await isServerRunning()) {
|
|
2272
2355
|
await startServer();
|
|
2273
2356
|
}
|
|
@@ -2308,7 +2391,31 @@ function registerCommands(program2) {
|
|
|
2308
2391
|
console.log(url);
|
|
2309
2392
|
openBrowser(url);
|
|
2310
2393
|
});
|
|
2311
|
-
program2.command("
|
|
2394
|
+
program2.command("ls [id]").alias("list").description("List all sessions, or filter by id substring").action(async (filter) => {
|
|
2395
|
+
let res;
|
|
2396
|
+
try {
|
|
2397
|
+
res = await fetch(`${BASE_URL}/api/sessions`);
|
|
2398
|
+
} catch {
|
|
2399
|
+
console.log("webtty is not running");
|
|
2400
|
+
process.exit(1);
|
|
2401
|
+
}
|
|
2402
|
+
const all = await res.json();
|
|
2403
|
+
const sessions = filter ? all.filter((s) => s.id.includes(filter)) : all;
|
|
2404
|
+
if (sessions.length === 0) {
|
|
2405
|
+
console.log("no sessions");
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
console.log("id\t\t\tconnected\tcreated");
|
|
2409
|
+
for (const s of sessions) {
|
|
2410
|
+
const created = new Date(s.createdAt).toLocaleString();
|
|
2411
|
+
console.log(`${s.id} ${s.connected} ${created}`);
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
program2.command("rm [id]").alias("remove").description("Destroy a session").action(async (id) => {
|
|
2415
|
+
if (!id) {
|
|
2416
|
+
console.error("webtty: rm requires a session id");
|
|
2417
|
+
process.exit(1);
|
|
2418
|
+
}
|
|
2312
2419
|
let res;
|
|
2313
2420
|
try {
|
|
2314
2421
|
res = await fetch(`${BASE_URL}/api/sessions/${encodeURIComponent(id)}`, {
|
|
@@ -2320,6 +2427,10 @@ function registerCommands(program2) {
|
|
|
2320
2427
|
}
|
|
2321
2428
|
if (res.status === 204) {
|
|
2322
2429
|
console.log(`removed ${id}`);
|
|
2430
|
+
if (res.headers.get("x-sessions-remaining") === "0") {
|
|
2431
|
+
await stopServer();
|
|
2432
|
+
console.log("no sessions remaining — webtty stopped");
|
|
2433
|
+
}
|
|
2323
2434
|
} else if (res.status === 404) {
|
|
2324
2435
|
console.error(`session ${id} not found`);
|
|
2325
2436
|
process.exit(1);
|
|
@@ -2328,7 +2439,11 @@ function registerCommands(program2) {
|
|
|
2328
2439
|
process.exit(1);
|
|
2329
2440
|
}
|
|
2330
2441
|
});
|
|
2331
|
-
program2.command("
|
|
2442
|
+
program2.command("mv [id] [new-id]").alias("move").alias("rename").description("Rename a session").action(async (id, newId) => {
|
|
2443
|
+
if (!id || !newId) {
|
|
2444
|
+
console.error("webtty: rename requires two arguments: [id] [new-id]");
|
|
2445
|
+
process.exit(1);
|
|
2446
|
+
}
|
|
2332
2447
|
let res;
|
|
2333
2448
|
try {
|
|
2334
2449
|
res = await fetch(`${BASE_URL}/api/sessions/${encodeURIComponent(id)}`, {
|
|
@@ -2351,23 +2466,84 @@ function registerCommands(program2) {
|
|
|
2351
2466
|
process.exit(1);
|
|
2352
2467
|
}
|
|
2353
2468
|
});
|
|
2354
|
-
program2.command("
|
|
2469
|
+
program2.command("stop").description("Stop the webtty server").action(async () => {
|
|
2470
|
+
if (!await isServerRunning()) {
|
|
2471
|
+
console.log("webtty is not running");
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
const ok = await stopServer();
|
|
2475
|
+
if (ok) {
|
|
2476
|
+
console.log("webtty stopped");
|
|
2477
|
+
} else {
|
|
2478
|
+
console.error("webtty stop failed");
|
|
2479
|
+
process.exit(1);
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
program2.command("start").description("Start the webtty server").action(async () => {
|
|
2355
2483
|
if (await isServerRunning()) {
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
console.error("webtty: failed to stop server");
|
|
2359
|
-
process.exit(1);
|
|
2360
|
-
}
|
|
2484
|
+
console.log("webtty is already running");
|
|
2485
|
+
return;
|
|
2361
2486
|
}
|
|
2362
2487
|
await startServer();
|
|
2363
|
-
console.log("webtty
|
|
2488
|
+
console.log("webtty started");
|
|
2489
|
+
});
|
|
2490
|
+
program2.command("config").description("Open the config file in $EDITOR").action(() => {
|
|
2491
|
+
const dir = configDir();
|
|
2492
|
+
const configPath = path3.join(dir, "config.json");
|
|
2493
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
2494
|
+
if (!fs3.existsSync(configPath)) {
|
|
2495
|
+
fs3.writeFileSync(configPath, `{}
|
|
2496
|
+
`, "utf8");
|
|
2497
|
+
}
|
|
2498
|
+
const editor = process.env.VISUAL ?? process.env.EDITOR ?? (process.platform === "win32" ? "notepad" : "vi");
|
|
2499
|
+
childProcess2.spawnSync(editor, [configPath], { stdio: "inherit" });
|
|
2500
|
+
});
|
|
2501
|
+
program2.command("help").description("Show help — all commands and options").action(() => {
|
|
2502
|
+
program2.outputHelp();
|
|
2364
2503
|
});
|
|
2365
2504
|
}
|
|
2366
2505
|
|
|
2367
2506
|
// src/cli/index.ts
|
|
2507
|
+
var CMDS_WITH_ARGS = new Set(["at", "rm", "ls", "mv"]);
|
|
2508
|
+
var CMD_NAME_WIDTH = "mv".length;
|
|
2368
2509
|
var program2 = new Command;
|
|
2369
|
-
program2.name("webtty").description("
|
|
2510
|
+
program2.name("webtty").description("Launch Terminal UI in the browser.").configureHelp({
|
|
2511
|
+
styleTitle(str) {
|
|
2512
|
+
return str.replace(/:$/, "").toUpperCase();
|
|
2513
|
+
},
|
|
2514
|
+
subcommandTerm(cmd) {
|
|
2515
|
+
const args = cmd.registeredArguments.map((arg) => arg.required ? `<${arg.name()}>` : `[${arg.name()}]`).join(" ");
|
|
2516
|
+
const name = CMDS_WITH_ARGS.has(cmd.name()) ? cmd.name().padEnd(CMD_NAME_WIDTH) : cmd.name();
|
|
2517
|
+
return args ? `${name} ${args}` : name;
|
|
2518
|
+
},
|
|
2519
|
+
formatHelp(cmd, helper) {
|
|
2520
|
+
const helpWidth = helper.helpWidth ?? 80;
|
|
2521
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
2522
|
+
const callFormatItem = (term, description2) => helper.formatItem(term, termWidth, description2, helper);
|
|
2523
|
+
const description = helper.commandDescription(cmd);
|
|
2524
|
+
const descriptionBlock = description.length > 0 ? ["", helper.boxWrap(helper.styleCommandDescription(description), helpWidth), ""] : [];
|
|
2525
|
+
const indent = " ";
|
|
2526
|
+
const usageWidth = "webtty [command]".length;
|
|
2527
|
+
const usageBlock = [
|
|
2528
|
+
helper.styleTitle("Usage:"),
|
|
2529
|
+
`${indent}${helper.styleUsage("webtty".padEnd(usageWidth))} ${helper.styleCommandDescription("Attach to main session and open it")}`,
|
|
2530
|
+
`${indent}${helper.styleUsage("webtty [command]".padEnd(usageWidth))} ${helper.styleCommandDescription("Execute a specific command")}`,
|
|
2531
|
+
""
|
|
2532
|
+
];
|
|
2533
|
+
const commandGroups = helper.groupItems(cmd.commands, helper.visibleCommands(cmd), (sub) => sub.helpGroup?.() || "Commands:");
|
|
2534
|
+
const commandsBlock = [];
|
|
2535
|
+
commandGroups.forEach((commands, group) => {
|
|
2536
|
+
const commandList = commands.map((sub) => callFormatItem(helper.styleSubcommandTerm(helper.subcommandTerm(sub)), helper.styleSubcommandDescription(helper.subcommandDescription(sub))));
|
|
2537
|
+
commandsBlock.push(...helper.formatItemList(group, commandList, helper));
|
|
2538
|
+
});
|
|
2539
|
+
return [...descriptionBlock, ...usageBlock, ...commandsBlock].join(`
|
|
2540
|
+
`);
|
|
2541
|
+
}
|
|
2542
|
+
});
|
|
2370
2543
|
registerCommands(program2);
|
|
2371
|
-
program2.
|
|
2544
|
+
program2.action(async () => {
|
|
2545
|
+
await program2.parseAsync(["at", "main"], { from: "user" });
|
|
2546
|
+
});
|
|
2547
|
+
program2.parseAsync(process.argv);
|
|
2372
2548
|
|
|
2373
|
-
//# debugId=
|
|
2549
|
+
//# debugId=1ED78924E352A12364756E2164756E21
|