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 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 run # start server + open a terminal in the browser
9
- npx webtty ls # list sessions
10
- npx webtty help # show all commands
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/http.ts
2160
- import * as childProcess from "node:child_process";
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 = path.dirname(__filename2);
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
- return true;
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 = path.resolve(__dirname2, isTs ? "../server/index.ts" : "../server/index.js");
2180
- if (!fs.existsSync(serverEntry)) {
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: "ignore",
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("start").description("Start the webtty server").action(async () => {
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("rm <id>").description("Kill a session and its PTY").action(async (id) => {
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("rename <id> <new-id>").description("Rename a session").action(async (id, newId) => {
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("restart").description("Restart the webtty server").action(async () => {
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
- const ok = await stopServer();
2357
- if (!ok) {
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 restarted");
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("Web TTY — run terminal sessions in a browser tab");
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.parse(process.argv);
2544
+ program2.action(async () => {
2545
+ await program2.parseAsync(["at", "main"], { from: "user" });
2546
+ });
2547
+ program2.parseAsync(process.argv);
2372
2548
 
2373
- //# debugId=1DEC1C353196B21C64756E2164756E21
2549
+ //# debugId=1ED78924E352A12364756E2164756E21