Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186#!/usr/bin/env node// jasellite — open a new macOS Terminal window running `claude` on the// remote jasellite box, registered so the slab menubar tracks and colorizes// it exactly like a native local session.//// How the slab integration works without touching the Swift app: slab themes// a Terminal window by matching a marker file's `tty` to the window and// checking its `claude_pid` is alive locally. A remote claude launched here// runs *inside a local Terminal window*, so that window has a local tty and// the transport (mosh/ssh/et) has a local pid. We record those here; the// companion bridge (claude-remote-bridge.mjs) mirrors jasellite's session// markers down with the tty/pid rewritten to these local ones.//// Usage:// jasellite open claude in the configured remote dir// jasellite <remote-dir> open claude cd'd into <remote-dir>// jasellite --et|--ssh|--mosh override transport for this launch// jasellite -r <session> resume a remote claude session by id//// Config (untracked): ~/.config/slab/remote-claude.json — see DEFAULTS below.import { execFileSync, spawn } from "node:child_process";import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";import { homedir } from "node:os";import { join, dirname } from "node:path";import { fileURLToPath } from "node:url";const HOME = homedir();const SLAB_STATE = join(HOME, ".local/share/slab/state");const LAUNCH_DIR = join(SLAB_STATE, "remote-launches");const CONFIG = join(HOME, ".config/slab/remote-claude.json");// Defaults are overridable per-key by ~/.config/slab/remote-claude.json.// `sshAlias` is the Host entry in ~/.ssh/config (works over the public IP or// the tailnet name). `etTarget` is user@host for Eternal Terminal.const DEFAULTS = { name: "jasellite", sshAlias: "jasellite", etTarget: "jas@24.144.92.66", transport: "mosh", // mosh | et | ssh — closest-to-resilient default remoteDir: "~/aesthetic-computer", terminalApp: "Terminal", // Terminal | iTerm2};function loadConfig() { if (!existsSync(CONFIG)) return { ...DEFAULTS }; try { return { ...DEFAULTS, ...JSON.parse(readFileSync(CONFIG, "utf8")) }; } catch (err) { console.warn(`jasellite: ignoring bad config (${err.message})`); return { ...DEFAULTS }; }}// Parse args: a bare arg is the remote dir; flags pick transport / resume.function parseArgs(argv) { const out = { dir: null, transport: null, session: null, dry: false }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === "--mosh" || a === "--et" || a === "--ssh") out.transport = a.slice(2); else if (a === "-s" || a === "--session") out.session = argv[++i]; else if (a === "--print" || a === "--dry") out.dry = true; else if (!a.startsWith("-")) out.dir = a; } return out;}// Single-quote a string for safe embedding in a bash -lc command.const shq = (s) => `'${String(s).replace(/'/g, "'\\''")}'`;// A filesystem-safe session slug from a remote dir (its last path component).const slug = (s) => s.replace(/^~\/?/, "").replace(/\/+$/, "").split("/").pop() .replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "home";// Build the command the local Terminal window runs: a transport hop into// jasellite that hands off to the box-side `jasellite-session` helper, which// attaches to (or creates) a named zellij session running claude. Keeping// claude inside zellij means a closed/dropped window reattaches to the live// session instead of starting fresh. The session name doubles as SLAB_LAUNCH_ID// (stable across reattach), so the bridge always maps it to the current window.function buildCommand(cfg, transport, sessionName, dir) { // Expand a leading ~ on the remote — single-quoting it would make it literal. let dirExpr; if (dir === "~" || dir === "~/") dirExpr = `"$HOME"`; else if (dir.startsWith("~/")) dirExpr = `"$HOME"/${shq(dir.slice(2))}`; else dirExpr = shq(dir); const helper = `"$HOME"/.local/bin/jasellite-session`; // On detach/exit drop into fish (jas's shell) so the window stays usable. const remote = `DIR=${dirExpr}; ` + `${helper} ${shq(sessionName)} "$DIR" ${shq(sessionName)}; ` + `exec fish -l`; const inner = `bash -lc ${shq(remote)}`; if (transport === "mosh") return `mosh ${cfg.sshAlias} -- ${inner}`; if (transport === "et") return `et ${cfg.etTarget} -c ${shq(inner)}`; return `ssh -t ${cfg.sshAlias} ${shq(inner)}`; // ssh}// Open a new Terminal/iTerm2 window running `cmd`; return the window's tty// (e.g. "ttys004"). Terminal.app's `do script` returns the tab it spawned, so// we can read its tty directly — that's the handle the bridge themes by.function openWindow(app, cmd) { const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); let script; if (app === "iTerm2") { // iTerm2 doesn't hand back a tty as cleanly; settle the session then read. script = `tell application "iTerm2" activate set w to (create window with default profile) tell current session of w to write text "${esc(cmd)}" delay 0.4 set theTty to tty of current session of wend tellreturn theTty`; } else { script = `tell application "Terminal" activate set theTab to do script "${esc(cmd)}" delay 0.3 set theTty to tty of theTabend tellreturn theTty`; } const out = execFileSync("/usr/bin/osascript", ["-e", script], { encoding: "utf8", }).trim(); return out.replace(/^\/dev\//, ""); // normalize "/dev/ttys004" -> "ttys004"}// Find the local transport process pid sitting on `tty` — that's the pid the// mirror marker uses for liveness, so closing the window marks it stale.// Make sure the marker bridge daemon is running; start it detached if not.function ensureBridge() { const pidFile = join(SLAB_STATE, "remote-bridge.pid"); try { const rec = JSON.parse(readFileSync(pidFile, "utf8")); if (rec?.pid) { process.kill(rec.pid, 0); return; } // already alive } catch {} const bridge = join(dirname(fileURLToPath(import.meta.url)), "claude-remote-bridge.mjs"); const child = spawn(process.execPath, [bridge], { detached: true, stdio: "ignore" }); child.unref();}function main() { const cfg = loadConfig(); const args = parseArgs(process.argv.slice(2)); const transport = args.transport || cfg.transport; const dir = args.dir || cfg.remoteDir; // Stable session name (default per dir) so re-running reattaches the same // zellij session. It doubles as the launch id the bridge correlates on, and // the launch record is rewritten each launch with the *current* window's // tty/pid — so a reattach from a fresh window re-points the mirror correctly. const sessionName = args.session || (args.dir ? `jasellite-${slug(args.dir)}` : "jasellite"); const cmd = buildCommand(cfg, transport, sessionName, dir); if (args.dry) { console.log(cmd); return; } const tty = openWindow(cfg.terminalApp, cmd); if (!tty || tty.startsWith("tty-not")) { console.error("jasellite: couldn't read the new window's tty"); process.exit(1); } mkdirSync(LAUNCH_DIR, { recursive: true }); // Liveness + the marker's pid are resolved from local_tty by the bridge each // tick (mosh's client is slow to appear, so a pid snapshot here is racy). const record = { launch_id: sessionName, session: sessionName, host: cfg.name, transport, local_tty: tty, remote_dir: dir, started: new Date().toISOString(), }; writeFileSync(join(LAUNCH_DIR, sessionName), JSON.stringify(record, null, 2)); ensureBridge(); console.log( `jasellite: ${transport} → ${cfg.name} · session ${sessionName} · ${dir}\n` + ` window tty ${tty} (close & re-run to reattach)`, );}main();