{"id":51193604,"url":"https://github.com/mmakam2/tmuxifier","last_synced_at":"2026-06-27T18:00:56.984Z","repository":{"id":366270101,"uuid":"1275038118","full_name":"mmakam2/tmuxifier","owner":"mmakam2","description":"Single-user web dashboard for managing headless boxes over SSH: browser terminals (xterm.js) backed by on-box tmux sessions that survive tab closes, network drops, and restarts. Stores no SSH secrets.","archived":false,"fork":false,"pushed_at":"2026-06-21T03:47:48.000Z","size":3299,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-21T04:21:34.962Z","etag":null,"topics":["dashboard","fastify","nodejs","self-hosted","ssh","terminal","tmux","web-terminal","xterm-js"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mmakam2.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-20T07:05:06.000Z","updated_at":"2026-06-21T03:47:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mmakam2/tmuxifier","commit_stats":null,"previous_names":["mmakam2/tmuxifier"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/mmakam2/tmuxifier","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmakam2%2Ftmuxifier","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmakam2%2Ftmuxifier/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmakam2%2Ftmuxifier/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmakam2%2Ftmuxifier/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mmakam2","download_url":"https://codeload.github.com/mmakam2/tmuxifier/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmakam2%2Ftmuxifier/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34862627,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dashboard","fastify","nodejs","self-hosted","ssh","terminal","tmux","web-terminal","xterm-js"],"created_at":"2026-06-27T18:00:24.652Z","updated_at":"2026-06-27T18:00:56.978Z","avatar_url":"https://github.com/mmakam2.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003cimg src=\"src/web/assets/tmuxifier-logo.png\" alt=\"\" width=\"36\" height=\"36\" style=\"vertical-align:middle\" /\u003e tmuxifier\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\nA single-user web dashboard for managing headless boxes over SSH. Each box opens a\nbrowser terminal backed by a tmux session that lives **on the box**, so closing the tab,\nlosing the network, or restarting Tmuxifier leaves your work running — reconnecting drops you\nback into the same state.\n\n## Screenshots\n\n| Login | Dashboard |\n|:---:|:---:|\n| [![Login screen](docs/screenshots/login.png)](docs/screenshots/login.png) | [![Dashboard with terminal](docs/screenshots/dashboard.png)](docs/screenshots/dashboard.png) |\n\n| Add Box |\n|:---:|\n| [![Add box dialog](docs/screenshots/add-box.png)](docs/screenshots/add-box.png) |\n\n## Requirements\n- Node 20+\n- The OpenSSH client, with your keys/agent/`~/.ssh/config` already working from the shell\n- Tmuxifier installs `tmux` when a box is added if the remote user is root or has passwordless\n  `sudo` for the system package manager\n\n## Setup\n```bash\nnpm install\nnpm run build\nnpm run set-password   # writes the password hash + cookie secret into ./.env\nnpm start\n```\nOpen http://127.0.0.1:7437.\n\nConfiguration lives in a gitignored **`.env` file in the repo root**, so Tmuxifier is\nself-contained — nothing needs to be set in your shell. `npm run set-password` creates (or\nupdates) `.env` with `TMUXIFIER_PASSWORD_HASH` and `TMUXIFIER_COOKIE_SECRET`; re-running it\nchanges the password while keeping the existing cookie secret (so you stay logged in). Copy\n`.env.example` to `.env` first if you want to set other options up front.\n\n## Configuration\nAll options are read from `.env` in the repo root (see `.env.example`). Each key can also be\nset as a real shell environment variable, which **overrides** the file. Precedence, low to\nhigh: built-in defaults → `config.json` → `.env` → shell environment.\n\n| Key | Env / `.env` key | Default |\n| --- | --- | --- |\n| bind address | `TMUXIFIER_BIND` | `127.0.0.1` |\n| port | `TMUXIFIER_PORT` | `7437` |\n| grace seconds | `TMUXIFIER_GRACE` | `45` |\n| host-key policy | `TMUXIFIER_HOSTKEY_POLICY` | `accept-new` |\n| status probe concurrency | `TMUXIFIER_STATUS_CONCURRENCY` | `4` |\n| status poll interval (ms) | `TMUXIFIER_STATUS_POLL_MS` | `30000` |\n| SSH ControlPersist seconds | `TMUXIFIER_CONTROL_PERSIST` | `600` |\n| terminal font family | `TMUXIFIER_TERM_FONT` | (bundled font) |\n| terminal font size (px) | `TMUXIFIER_TERM_FONT_SIZE` | `10` |\n| fleet command concurrency | `TMUXIFIER_FLEET_CONCURRENCY` | `4` |\n| fleet per-box timeout (ms) | `TMUXIFIER_FLEET_TIMEOUT_MS` | `15000` |\n| fleet job history kept | `TMUXIFIER_FLEET_MAX_JOBS` | `50` |\n| fleet per-box output cap (bytes) | `TMUXIFIER_FLEET_MAX_OUTPUT_BYTES` | `65536` |\n| Proxmox task poll interval (ms) | `TMUXIFIER_PVE_POLL_MS` | `1500` |\n| Proxmox per-request timeout (ms) | `TMUXIFIER_PVE_TIMEOUT_MS` | `15000` |\n| Proxmox provision timeout (ms) | `TMUXIFIER_PVE_PROVISION_TIMEOUT_MS` | `600000` |\n| Proxmox DHCP-lease wait (ms) | `TMUXIFIER_PVE_LEASE_TIMEOUT_MS` | `60000` |\n| Proxmox provision job history kept | `TMUXIFIER_PVE_MAX_JOBS` | `50` |\n| Proxmox default management pubkey | `TMUXIFIER_PVE_DEFAULT_PUBKEY` | auto-detect `~/.ssh/*.pub` |\n| auth mode | `TMUXIFIER_AUTH_MODE` | `password` |\n| password hash | `TMUXIFIER_PASSWORD_HASH` | — (required) |\n| cookie secret | `TMUXIFIER_COOKIE_SECRET` | — (required) |\n| base external URL | `TMUXIFIER_BASE_EXTERNAL_URL` | (none) |\n| OAuth client id | `TMUXIFIER_OAUTH_CLIENT_ID` | (none) |\n| OAuth client secret | `TMUXIFIER_OAUTH_CLIENT_SECRET` | (none) |\n| allowed Google emails | `TMUXIFIER_ALLOWED_EMAILS` | (none) |\n| data dir | `TMUXIFIER_DATA_DIR` | `\u003crepo\u003e/data` |\n| control-socket dir | `TMUXIFIER_CONTROL_DIR` | `\u003cdataDir\u003e/cm` |\n| ssh config for Tmuxifier SSH calls | `TMUXIFIER_SSH_CONFIG` | (none) |\n| TLS cert (PEM) | `TMUXIFIER_TLS_CERT` | (none → serves HTTP) |\n| TLS key (PEM) | `TMUXIFIER_TLS_KEY` | (none → serves HTTP) |\n\nSet **both** `TMUXIFIER_TLS_CERT` and `TMUXIFIER_TLS_KEY` to serve HTTPS directly; when TLS is active\nthe session cookie is automatically marked `Secure`. An `https://` `TMUXIFIER_BASE_EXTERNAL_URL`\nalso marks it `Secure` for deployments behind a TLS-terminating proxy or tunnel.\n\nAs an alternative to `.env`, a `config.json` in the repo root works too, using camelCase keys\n(`passwordHash`, `cookieSecret`, `bindAddress`, `port`, `graceSeconds`, `hostKeyPolicy`,\n`statusConcurrency`, `statusPollMs`, `controlPersist`, `termFont`, `termFontSize`, `fleetConcurrency`, `fleetTimeoutMs`,\n`fleetMaxJobs`, `fleetMaxOutputBytes`, `pvePollMs`, `pveTimeoutMs`, `pveProvisionTimeoutMs`,\n`pveLeaseTimeoutMs`, `pveMaxJobs`, `pveDefaultPubKeyPath`, `authMode`, `publicUrl`, `googleClientId`,\n`googleClientSecret`, `allowedEmails`, `dataDir`, `controlDir`, `sshConfigFile`, `tlsCert`,\n`tlsKey`). The UI also persists `localShell` in `config.json`; it does not have an env key.\n`TMUXIFIER_SSH_CONFIG`/`sshConfigFile` is passed to `ssh` as `-F`, so it is an alternate config\nfile for Tmuxifier's SSH commands, not an extra file merged with `~/.ssh/config`.\n\n`TMUXIFIER_TERM_FONT` sets the font for the browser **terminal sessions** (not the dashboard\nchrome). It is a single family name, prepended to the bundled font stack, so it must be installed\non the device viewing the dashboard — otherwise that device transparently falls back to the bundled\n**MesloLGMDZ Nerd Font** (Line Gap Medium, dotted zero, the default terminal font). An unsafe or\nempty value is ignored. The bundled fonts (MesloLGMDZ, then MesloLGSDZ and JuliaMono) always remain\nas the fallback, so symbol glyphs (e.g. Claude Code's UI) keep rendering regardless of the choice.\n\nThe sidebar's **export** (⤓) and **import** (⤒) buttons download and upload the full box list as a\nJSON file — a portable backup you can move between Tmuxifier instances. Import adds boxes from the\nfile, re-minting each id and skipping any whose host/label already exists (so re-importing is safe).\nIt carries no SSH secrets; boxes still rely on your keys/agent/`~/.ssh/config` at connect time.\n\n## Authentication\n`TMUXIFIER_AUTH_MODE` selects one login method. The default is `password`; set it to\n`oauth` to replace the password form with Google sign-in. The modes are exclusive.\n\nPassword mode:\n```bash\nnpm run set-password\n```\nThis writes `TMUXIFIER_PASSWORD_HASH` and, if absent, `TMUXIFIER_COOKIE_SECRET` to `.env`.\n\nOAuth mode:\n```bash\nnpm run gen-secret\n```\nThen set these `.env` keys:\n```ini\nTMUXIFIER_AUTH_MODE=oauth\nTMUXIFIER_BASE_EXTERNAL_URL=tmuxifier.example.com\nTMUXIFIER_OAUTH_CLIENT_ID=...\nTMUXIFIER_OAUTH_CLIENT_SECRET=...\nTMUXIFIER_ALLOWED_EMAILS=you@example.com,teammate@example.com\n```\nTmuxifier treats a scheme-less public URL as HTTPS. In Google Cloud Console, create an OAuth\nclient ID for a web application and register this\nauthorized redirect URI:\n```text\nhttps://tmuxifier.example.com/api/auth/google/callback\n```\nThe allowlist is exact email addresses only, matched case-insensitively. Domain wildcards are\nnot supported. The older `TMUXIFIER_PUBLIC_URL`, `TMUXIFIER_GOOGLE_CLIENT_ID`,\n`TMUXIFIER_GOOGLE_CLIENT_SECRET`, and `TMUXIFIER_AUTH_MODE=google` names are still accepted.\n\n## How persistence works\nEach terminal runs `ssh -tt \u003cbox\u003e \"tmux new-session -A -D -s \u003csession\u003e\"` (the `-D` detaches any\nother client so a stale connection can't freeze the layout). `\u003csession\u003e` is the box's tmux session\nname — set per box in the Add/Edit dialog (a type-or-pick field whose ⟳ button fetches the host's\nlive sessions), defaulting to `web`. Because tmux runs on the box, the session and its processes\nsurvive disconnects. A 45s server-side grace window makes brief reconnects seamless; after that\nthe local ssh process is dropped while the on-box session keeps running.\n\nWhen a box is added, Tmuxifier persists the box immediately and opens a live provisioning\npanel. That provisioning flow checks for `tmux`, installs it through a known package manager\nwhen possible (`apt-get`, `dnf`, `yum`, `pacman`, `apk`, or `zypper`), applies any selected\nshell/theme options, and creates the configured tmux session. If provisioning exits non-zero,\nthe new box is rolled back from the list. Removing a box closes any local terminal process for\nthat box and best-effort kills the configured remote tmux session before deleting the box.\n\n## Status, multiplexing \u0026 rate-limit safety\nTmuxifier talks to each box over SSH continuously — a background **status probe** keeps the\nsidebar dots current, and each open terminal is another SSH connection. Left naive, that churn\n(a fresh handshake, plus a failed auth on password boxes, every few seconds) is exactly what\ntrips a box's brute-force protection — `fail2ban`, `sshguard`, or a connection-rate firewall\nrule — and gets the Tmuxifier host's IP **banned**, which then makes the box look dead. Several\nmechanisms keep the connection rate low and reuse one warm connection:\n\n- **One shared poll, not one per tab.** Status is probed by a single **server-side** loop (every\n  `TMUXIFIER_STATUS_POLL_MS`, default 30s); every open dashboard tab reads the same cached snapshot\n  instead of driving its own probe cycle, so the SSH connection rate does **not** multiply with the\n  number of tabs you leave open. Concurrent probes of the same box are also coalesced into one\n  connection. (Before this, several open tabs could fan out enough simultaneous handshakes to arm a\n  box's rate limiter.)\n- **Connection multiplexing (keep one warm).** Every probe and terminal for a box shares a\n  single persistent SSH **ControlMaster** socket under `data/cm/`, authenticated once and kept\n  alive for `TMUXIFIER_CONTROL_PERSIST` seconds (default 600) after its last use. Repeated\n  status checks and reconnects ride that one connection instead of re-authenticating — no\n  per-probe handshake, no per-probe auth attempt.\n- **Adaptive status backoff.** Probing starts at the ~30s poll cadence, but each consecutive\n  failure *escalates* the interval (30s → 60s → … up to a **5-minute floor**), and a box that\n  needs a password jumps straight to the 5-minute floor — fast probing there can never succeed\n  and only feeds `fail2ban`. It never fully stops, so a box that recovers turns green on its own\n  within ≤5 minutes. A successful check, or opening/reconnecting the box, resets it to the fast\n  cadence.\n- **Don't probe a box you're using.** While a terminal session is open for a box, the status\n  probe is skipped entirely — the dot is read from the live ControlMaster instead (master up ⇒\n  connected; absent ⇒ needs auth) — so a probe can't collide with your interactive login on the\n  shared socket.\n- **Fail fast, then back off.** Both probes and interactive connects set an SSH `ConnectTimeout`\n  (≈6s / 10s) so an unreachable box fails quickly instead of hanging. The browser terminal then\n  reconnects on its own escalating backoff to a **5-minute floor** — a box left open while it's\n  down settles to roughly one attempt every five minutes (gentle enough not to arm a limiter)\n  and auto-reconnects within ≤5 minutes of coming back. A connection that proves stable resets\n  the backoff to fast.\n- **Bounded fan-out.** A full status sweep probes boxes in small batches\n  (`TMUXIFIER_STATUS_CONCURRENCY`, default 4), so the dashboard never opens a fleet-wide burst of\n  simultaneous handshakes.\n\nIf a box still bans the Tmuxifier host (a red dot that pings but times out on port 22), the bans\nare time-limited — the low, backed-off connection rate lets them expire instead of continually\nre-arming them. To clear one immediately, unban the Tmuxifier host's IP on that box\n(e.g. `fail2ban-client unbanip \u003cip\u003e`) and consider allowlisting it (`ignoreip`).\n\n### Fleet Command\n\nClick **Fleet** in the sidebar to enter selection mode, tick any number of boxes (or whole tag\ngroups), type a command, and **Run**. The command runs once on each selected box over the same\nnon-interactive SSH path used for status probes, and each box's exit code and output are captured\ncentrally. Each run is a **job** held on the server: close the tab and the run keeps going —\nreopen the dashboard and the **Jobs** button lists recent jobs with their per-box results. Jobs\nare persisted to `data/fleet-jobs.json` (last `TMUXIFIER_FLEET_MAX_JOBS`, default 50). The fan-out\nis capped at `TMUXIFIER_FLEET_CONCURRENCY` (default 4) so a fleet-wide run never bursts SSH\nconnections. Password-only boxes with no live connection come back as a per-box error (the\nnon-interactive path can't answer a password prompt) — open that box's terminal once to establish\nthe connection, then re-run.\n\n## Proxmox LXC provisioning\n\nTmuxifier can provision a \"canned\" LXC container on a Proxmox VE host over the PVE HTTP API and\nauto-add a box pointed at it, so a freshly created container opens straight into a browser terminal.\n\n**1. Create an API token in Proxmox.** *Datacenter → Permissions → API Tokens → Add*. Pick a\nuser/realm (e.g. `user@pam`), a token id (e.g. `tmuxifier`), and copy the secret (shown once).\n**Grant the token its own permissions** — tokens default to \"Privilege Separation\", so the token has\nno rights even when the user does. In a lab, add the token (*Datacenter → Permissions → Add → API\nToken Permission*, path `/`, propagate) the built-in **`PVEVMAdmin`** role (container create/start\nplus `Datastore.AllocateSpace`/`Datastore.Audit`) **and `PVEAuditor`** (the `Sys.Audit` that lets\nthe node/storage/bridge dropdowns populate). Use a privilege-separated token, not full\n`Administrator`.\n\n**2. Add the host.** Dashboard → **Proxmox → Hosts → Add**: enter the endpoint (`host:8006`), the\ntoken id (`user@pam!tmuxifier`) and the secret. Click **Inspect** to fetch and **pin** the host's\nTLS certificate (Proxmox ships a self-signed cert; pinning is trust-on-first-use, like\n`ssh accept-new`). Save — Tmuxifier verifies the token before storing it.\n\n**3. Review LXC Secrets.** **Proxmox → LXC Secrets**. Tmuxifier's own host key is auto-detected and\nshown as the **default management key** — injected into every container so Tmuxifier can SSH in (set\n`TMUXIFIER_PVE_DEFAULT_PUBKEY` if your key isn't at `~/.ssh/id_*`). Optionally add more **public\nkeys** (e.g. your laptop's) and/or an **optional root password**. Added keys and the password are\nencrypted at rest and shown masked after saving; the private half of any key stays in your own SSH\nsetup — Tmuxifier never stores private keys.\n\n**4. Define a preset and provision.** **Presets → Add** a blueprint (template, CPU/mem/disk,\nstorage, network). Then **Provision → pick a preset → enter a hostname** (optionally a tag and\noh-my-tmux/zsh/bash). Watch the live task log; once the container is up Tmuxifier installs tmux (and\nany selected frameworks) over SSH, then an **Open terminal** button drops you into it.\n\n**Security.** The API token, any added SSH keys, and the optional root password are **encrypted at\nrest** (AES-256-GCM; key derived from your cookie secret) in the gitignored `data/proxmox.json`\n(`0600`), and are never sent to the browser. TLS is pinned for self-signed certs and CA-verified\nwhen the host presents a valid certificate. If you rotate `TMUXIFIER_COOKIE_SECRET`, previously-saved\nsecrets become undecryptable — re-add each Proxmox host (and re-enter keys/password) afterward.\n\n## Security\nTmuxifier can SSH into your whole fleet, so the login gate is the crown jewel. It binds to\n`127.0.0.1` by default. To expose it on a network, **always use TLS** — either set\n`TMUXIFIER_TLS_CERT`/`TMUXIFIER_TLS_KEY` to serve HTTPS directly (a self-signed cert works; browsers\nshow a one-time warning), or front it with a TLS reverse proxy — and set `TMUXIFIER_BIND`\naccordingly. Serving the login over plain HTTP on a non-loopback address sends credentials\nin cleartext. Passwords are scrypt-hashed; OAuth mode uses an exact-email allowlist; the\nsession cookie is signed, httpOnly, SameSite=lax, and marked `Secure` for local TLS or an\n`https://` base external URL. Tmuxifier stores no SSH secrets — your keys and agent stay in the OS.\n\nGenerate a self-signed cert (valid for an IP) with:\n```bash\nopenssl req -x509 -newkey rsa:2048 -nodes -days 825 \\\n  -keyout key.pem -out cert.pem -subj \"/CN=tmuxifier\" \\\n  -addext \"subjectAltName=IP:192.168.1.10,IP:127.0.0.1,DNS:localhost\"\n```\n\n## Deployment\nRun Tmuxifier as a long-lived **systemd** service. A deployment is just a checkout of the repo\nplus a small unit that runs `node src/server/index.js` from it — config (`.env`), certs\n(`tls/`), and state (`data/`) all stay inside the repo folder.\n\nThe repo ships a ready-to-use unit at [deploy/tmuxifier.service](deploy/tmuxifier.service),\nwhich assumes the repo is at `/root/tmuxifier` running as `root`:\n\n```ini\n[Unit]\nDescription=Tmuxifier - web dashboard for managing SSH/tmux boxes\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nUser=root\nWorkingDirectory=/root/tmuxifier\n# HOME must be set so the ssh children find ~/.ssh (keys, config, known_hosts)\nEnvironment=HOME=/root\nExecStart=/usr/bin/node /root/tmuxifier/src/server/index.js\nRestart=on-failure\nRestartSec=2\nNoNewPrivileges=true\n\n[Install]\nWantedBy=multi-user.target\n```\n\nInstall and start it (after `npm install \u0026\u0026 npm run build \u0026\u0026 npm run set-password`):\n\n```bash\nsudo cp deploy/tmuxifier.service /etc/systemd/system/tmuxifier.service\n# Not running from /root/tmuxifier as root? Edit User=, WorkingDirectory=,\n# Environment=HOME=, and the node path in ExecStart= to match your install.\nsudo systemctl daemon-reload\nsudo systemctl enable --now tmuxifier   # start now + on boot\nsystemctl status tmuxifier              # confirm it is active\n```\n\nTwo things to know: the app reads `.env` itself, so secrets are deliberately **not** placed in\nthe unit (it holds no credentials); and `HOME` is set in the unit — not `.env` — so the `ssh`\nchild processes can find `~/.ssh`. To update a running deployment: `git pull`, `npm install`\n(only if dependencies changed), `npm run build`, then `sudo systemctl restart tmuxifier`.\n\nSee [docs/DEPLOY.md](docs/DEPLOY.md) for the full guide — passwordless SSH key setup, TLS certs,\nGoogle OAuth behind a Cloudflare tunnel, the file-layout table, and password rotation.\n\n## Attributions\n\nTmuxifier can optionally install and configure these excellent projects on your boxes during\nprovisioning:\n\n| Project | Repository | What it does |\n| --- | --- | --- |\n| **Oh My Zsh** | [ohmyzsh/ohmyzsh](https://github.com/ohmyzsh/ohmyzsh) | Zsh framework with plugins, themes, and helpers |\n| **Oh My Bash** | [ohmybash/oh-my-bash](https://github.com/ohmybash/oh-my-bash) | Bash framework with themes and completions |\n| **Oh My Tmux** | [gpakosz/.tmux](https://github.com/gpakosz/.tmux) | Tmux configuration by Gregory Pakosz |\n\nEach installs via its upstream bootstrap script and is skipped if already present on the box.\n\n## Development\n```bash\nnpm run dev    # vite + node --watch, proxying /api and /term to the backend\nnpm test       # unit + integration (Vitest)\nnpm run test:e2e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmakam2%2Ftmuxifier","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmmakam2%2Ftmuxifier","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmakam2%2Ftmuxifier/lists"}