remobi 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-03-15
4
+
5
+ - Changed: overlay is now pre-built as an IIFE during `build:dist` — faster `remobi serve` startup (no runtime esbuild), smaller install footprint (esbuild moved to devDependencies). Dev mode falls back to esbuild-from-source when `dist/overlay.iife.js` is absent.
6
+
7
+ - Breaking: migrated from Bun to Node.js 22+ with pnpm. Runtime is now Node, bundler is esbuild, test runner is vitest, transpiler is tsdown. `remobi serve` uses Hono + @hono/node-ws for HTTP/WS. Package now ships transpiled JS (`dist/`) instead of TypeScript source.
8
+
9
+ - Internal: expanded oxlint — `suspicious` + `perf` categories, `import/no-cycle`, `import/no-self-import`, `typescript/no-non-null-asserted-optional-chain`, `unicorn/throw-new-error` (141 rules total).
10
+ - Internal: expanded Biome — `noExcessiveCognitiveComplexity` (warn, max 25), `useFilenamingConvention` (kebab-case).
11
+ - Internal: added knip for unused export/dependency detection. Removed 11 unused exports, removed redundant `happy-dom` devDep.
12
+ - Internal: added publint for npm package validation. Fixed `pkg.repository.url` to `git+https://` convention.
13
+ - Internal: added typos spell checker via mise.
14
+ - Internal: added hk pre-commit hooks (biome, oxlint, typos).
15
+ - Internal: added knip, publint, typos to CI pipeline and prepublishOnly gate.
16
+ - Changed: removed `ttyd` from `mise.toml` (unsupported on macOS arm64 via current aqua metadata) and updated ttyd install guidance to be macOS/Linux friendly (`brew` on macOS, distro/source options on Linux).
17
+ - Changed: shipped drawer defaults now stick to stock tmux bindings for split/session/window/copy actions and no longer include opinionated `Git`, `Files`, or `Links` buttons.
18
+
19
+ - Removed: plugin system (`RemobiPlugin`, `UISlot`, `UIContributionCollector`, plugin manager, UI contributions, build-time resolution, `config.plugins`). Hooks and actions remain as core infrastructure. The plugin API can be reintroduced when there's a concrete second use case.
20
+
21
+ - Fixed: visibilitychange listener leak in reconnect dispose path — the anonymous listener was never removed, causing a leak on each dispose/re-init cycle.
22
+ - Fixed: reconnect overlay now retries on any overlay tap, keeps the button focused for keyboard `Enter`, and guards against duplicate reload attempts.
23
+ - Fixed: unhandled promise rejection when `document.fonts.ready` fails — font loading failure is non-critical, terminal still works.
24
+ - Internal: `bun test --coverage` reporting via `test:coverage` script.
25
+ - Internal: shared `mockTerminal` test fixture extracted from 9 test files.
26
+ - Internal: new tests for `applyTheme`, `haptic`, and `checkLandscapeKeyboard`.
27
+
28
+ - Added: reconnect overlay — detects connection loss via WebSocket interception and shows a full-screen "Connection lost" overlay with a Reconnect button. Auto-reconnects when the browser comes back online. Enabled by default (`reconnect: { enabled: true }`), disable with `reconnect: { enabled: false }`.
29
+
30
+ - Added: Remotion-based demo video in `demo/` — programmatically rendered, Catppuccin Mocha themed.
31
+ - Added: oxlint with `consistent-type-assertions: never` rule to prevent unsafe type assertions.
32
+ - Changed: `waitForTerm` now rejects after timeout (default 10s) instead of polling indefinitely.
33
+ - Changed: plugin manager validates plugin shape at init — invalid plugins are skipped with a warning.
34
+ - Changed: help overlay rewritten to DOM API (no innerHTML), eliminating XSS surface.
35
+ - Changed: PWA meta-tag values are now HTML-attribute-escaped.
36
+ - Added: unit tests for `buildTtydArgs`, `randomInternalPort`, `waitForTerm`, and plugin validation.
37
+ - Added: `bun run build` step in CI workflow.
38
+ - Added: `remobi serve --no-sleep` flag — prevents macOS system sleep while serving by wrapping ttyd with `caffeinate -s -w <pid>`. The assertion is held for exactly the lifetime of the server and dropped automatically on shutdown. Gracefully ignored with a warning on non-macOS platforms.
39
+ - Added: keep-awake guide (`docs/guides/keep-awake.md`) covering `--no-sleep`, persistent pmset settings, nix-darwin config, and lid-close behaviour.
40
+ - Added: mobile-friendly tmux config guide (`docs/guides/mobile-tmux.md`) and optional tmux optimisation step in setup skill.
41
+ - Added: agent setup skill (`skills/remobi-setup/SKILL.md`), guide (`docs/guides/agent-setup.md`), and README collapsible prompt for AI-assisted configuration.
42
+ - Added: `remobi serve` — single command to run remobi with full PWA support. Builds overlay in memory, manages ttyd lifecycle, serves manifest + icons + WebSocket relay. Replaces the multi-step build + ttyd + proxy workflow.
43
+ - Added: PWA support — web app manifest, 192/512px icons, apple-touch-icon, theme-color meta tags for "Add to Home Screen" installability on iOS and Android.
44
+ - Added: `pwa` config section (`enabled`, `shortName`, `themeColor`) — controls manifest generation and meta tag injection. `shortName` defaults to `name` when absent.
45
+ - Added: top-level `name` config field (default `'remobi'`) — used as document title, PWA manifest name, and apple-mobile-web-app-title. Replaces `pwa.name` and `pwa.shortName` (the latter is now optional and falls back to `name`).
46
+ - Added: default toolbar backspace button (`⌫`, sends `\x7f`) to provide reliable deletion on mobile keyboards when IME composition behaviour is inconsistent.
47
+ - v0.2 extensibility and DX milestone complete: action registry, hook system, plugin manager, UI contributions, declarative button customisation, .local config overrides, plugin guide, Bun-only ADR, e2e scaffolding. Closes #1.
48
+ - Added: e2e smoke test scaffold (`tests/e2e/smoke.test.ts`) — checks ttyd availability, skips gracefully when absent, tests HTML serving and `remobi inject` pipe path against a real ttyd process. Closes #9.
49
+ - Added: ADR `docs/decisions/001-bun-only.md` — documents the decision to remain Bun-only, the Bun-specific APIs in use, and the conditions under which a Node runtime track would be considered. Closes #11.
50
+ - Added: plugin author guide (`docs/guides/plugins.md`) covering hooks, UI contributions, custom actions, and cleanup patterns. Closes #10.
51
+ - Added: stable public API surface defined in README — semver policy documents which import paths are public, what constitutes major/minor/patch. Closes #4.
52
+ - Added: UI contribution API for plugins — `context.ui.add(slot, button, priority?)` lets plugins contribute buttons to `'toolbar.row1'`, `'toolbar.row2'`, or `'drawer'` slots. Contributions are merged (appended) after config buttons, sorted by priority. Closes #8.
53
+ - Added: per-machine config overrides via `.local` config file — place `remobi.config.local.ts` next to your shared `remobi.config.ts` to apply machine-specific overrides (gitignore the `.local` file). Merged on top of the shared config using the same `RemobiConfigOverrides` schema. Closes #12.
54
+ - Added: declarative button customisation — toolbar rows and drawer buttons now accept `ButtonArrayInput`: a plain array (replace) or a function `(defaults) => newArray`. Standard JS array methods cover all customisation needs (filter, map, spread, etc.). Closes #13.
55
+
56
+ - Breaking: unified toolbar/drawer model to `ControlButton` (`id`, `label`, `description`, `action`) and renamed `drawer.commands` to `drawer.buttons`.
57
+ - Changed: touch scrolling defaults to wheel semantics for better behaviour across OpenCode, Claude Code, and plain tmux shells.
58
+ - Added: dynamic help overlay rendered from current config (no stale hardcoded sections).
59
+ - Fixed: help overlay is now fail-safe and cannot block core overlay init.
60
+ - Fixed: viewport/keyboard height handling and document scroll lock to reduce white-gap/rubber-band issues on mobile.
61
+ - Added: runtime config validation with path-based errors and unknown-key checks at CLI load boundaries.
62
+ - Changed: config validation errors now include received-value previews, and CLI validates merged resolved config before build/inject execution.
63
+ - Added: stricter CLI parsing (`-c`/`-o`/`-n`, unknown-flag errors) plus `--dry-run` for `build` and `inject`. Closes #3.
64
+ - Changed: toolbar/drawer button handling now runs through a shared action registry abstraction. Closes #5.
65
+ - Added: typed hook registry for overlay lifecycle and terminal send pipeline with ordered execution and error isolation. Closes #6.
66
+ - Added: plugin manager primitives (`RemobiPlugin`, setup/dispose lifecycle, failure isolation) and plugin-aware `init(..., hooks, plugins)` entry. Closes #7.
67
+ - Added: config `plugins` array support in CLI build/inject path with resolved local specifiers.
68
+ - Changed: CLI config validation remains strict; legacy config shapes are not auto-normalised.
69
+ - Tests: expanded integration/config/height coverage for the new config model and viewport logic.
70
+ - Added: `gestures.swipe.left`/`right` (data to send on swipe) and `leftLabel`/`rightLabel` (help overlay text) — defaults match previous hardcoded behaviour (`\x02n`/`\x02p`, next/previous tmux window).
71
+ - Added: `mobile.initData` (string | null) — arbitrary data sent to the terminal on mobile init when viewport is below `mobile.widthThreshold` (default 768px). Useful for auto-zooming a tmux pane (`'\x02z'`) or any other mobile-specific setup.
72
+ - Added: `floatingButtons` config — always-visible buttons on touch devices using the same `ControlButton` schema as toolbar/drawer. Renders in the help overlay when non-empty.
73
+ - Breaking: `floatingButtons` changed from a flat `ControlButton[]` to `FloatingButtonGroup[]`. Each group has a `position` (`'top-left' | 'top-right' | 'top-centre' | 'bottom-left' | 'bottom-right' | 'bottom-centre' | 'centre-left' | 'centre-right'`), optional `direction` (`'row' | 'column'`, default `'row'`), and a `buttons` array. Migrate: `floatingButtons: [btn]` → `floatingButtons: [{ position: 'top-left', buttons: [btn] }]`.
74
+ - Changed: Tailscale Serve guide now recommends always rebuilding the overlay before start; removed version-hash cache-key snippet to prevent stale build issues.
75
+ - Changed: default toolbar now uses explicit tmux `Prefix` (`C-b`) and `Alt+Enter` buttons for more predictable mobile input.
76
+ - Added: new drawer `Combo` action (`combo-picker`) that opens a small combo input modal for explicit Ctrl/Alt key sends.
77
+ - Added: default toolbar `q` button (row 2, left) for quitting interactive TUIs.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Connor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,302 @@
1
+ # remobi
2
+
3
+ [![CI](https://github.com/connorads/remobi/actions/workflows/ci.yml/badge.svg)](https://github.com/connorads/remobi/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/remobi)](https://www.npmjs.com/package/remobi)
5
+ [![licence](https://img.shields.io/npm/l/remobi)](LICENSE)
6
+
7
+ **Your terminal. Everywhere.**
8
+
9
+ Running coding agents in tmux? remobi lets you monitor and control them from your phone. Swipe between windows, scroll through output, pinch to zoom, tap buttons for tmux commands. Same session, touch-native controls. No workflow changes.
10
+
11
+ [ttyd](https://github.com/tsl0922/ttyd) gives you a terminal in a browser. On mobile, it's unusable — no toolbar, no gestures, tiny unresizable text. remobi adds a touch overlay on top. One command. Install it like a native app.
12
+
13
+ <div align="center">
14
+ <!-- Upload demo/out/demo.mp4 via GitHub issue/PR drag-and-drop, then replace the src below -->
15
+ <video src="https://github.com/user-attachments/assets/PLACEHOLDER" width="300" autoplay loop muted playsinline></video>
16
+ </div>
17
+
18
+ ## Who this is for
19
+
20
+ - You run coding agents (Claude Code, OpenCode, Codex, pi, etc.) in tmux and want to monitor or interact from your phone
21
+ - You're a terminal-first developer who wants your full tmux setup accessible anywhere
22
+ - You want self-hosted mobile terminal access without changing your existing workflow
23
+
24
+ ## Why remobi
25
+
26
+ - **One command** — `remobi serve` builds the overlay, manages ttyd, serves with PWA support
27
+ - **Swipe between panes** — gesture navigation, no prefix key fumbling on a phone screen
28
+ - **Pinch to zoom** — resize text like every other app on your phone
29
+ - **Install to your home screen** — standalone PWA, looks and feels native
30
+ - **Config-driven** — your buttons, your gestures, your layout. Or let an AI agent configure it for you
31
+ - **Self-hosted** — your data never leaves your network. Bring your own tunnel (Tailscale, Cloudflare, ngrok)
32
+
33
+ ## Requirements
34
+
35
+ - [Node.js](https://nodejs.org/) ≥ 22
36
+ - [ttyd](https://github.com/tsl0922/ttyd) — must be on PATH for `remobi serve` and `remobi build` (they spawn a temporary ttyd to fetch base HTML). Install on macOS with `brew install ttyd`; on Linux use your distro package manager or build from source via the [ttyd installation guide](https://github.com/tsl0922/ttyd#installation). `remobi inject` pipes HTML from stdin and does **not** require ttyd — useful for CI or environments where ttyd isn't installed locally.
37
+ - [tmux](https://github.com/tmux/tmux) (the target multiplexer)
38
+
39
+ remobi uses standard ttyd flags (`--writable`, `-t`, `-i`) and should work with any recent ttyd release.
40
+
41
+ ## Quick start
42
+
43
+ ```bash
44
+ # 1. Install
45
+ npm install -g remobi
46
+
47
+ # 2. Start (builds overlay, manages ttyd, serves with PWA support)
48
+ remobi serve
49
+ ```
50
+
51
+ For local development, use `pnpm link --global` from the repo root instead of `npm install -g remobi`.
52
+
53
+ Open `http://localhost:7681` on your phone. Add to Home Screen for an app-like experience.
54
+
55
+ ## CLI reference
56
+
57
+ ```
58
+ remobi serve [--config <path>] [--port <n>] [-- <command...>]
59
+ Build overlay in memory, manage ttyd, serve with PWA support.
60
+ Default port: 7681. Default command: tmux new-session -A -s main
61
+ Example: remobi serve --port 8080 -- tmux new -As dev
62
+
63
+ remobi build [--config <path>] [--output <path>] [--dry-run]
64
+ Build patched index.html for ttyd --index flag (advanced).
65
+ Default output: dist/index.html
66
+
67
+ remobi inject [--config <path>] [--dry-run]
68
+ Pipe mode: reads ttyd HTML from stdin, outputs patched HTML to stdout.
69
+ Example: curl -s http://localhost:7681/ | remobi inject > patched.html
70
+
71
+ remobi init
72
+ Scaffold a remobi.config.ts with commented defaults.
73
+
74
+ remobi --version
75
+ remobi --help
76
+ ```
77
+
78
+ Short flags: `-c` (`--config`), `-o` (`--output`), `-p` (`--port`), `-n` (`--dry-run`).
79
+
80
+ ### Config resolution
81
+
82
+ When `--config` is not specified, remobi searches:
83
+
84
+ 1. `remobi.config.ts` / `.js` in the current directory
85
+ 2. `~/.config/remobi/remobi.config.ts` / `.js` (XDG fallback)
86
+
87
+ ## Configuration
88
+
89
+ Create `remobi.config.ts` (or run `remobi init`):
90
+
91
+ ```typescript
92
+ import { defineConfig } from 'remobi/config'
93
+
94
+ export default defineConfig({
95
+ font: {
96
+ family: 'JetBrainsMono NFM, monospace',
97
+ mobileSizeDefault: 16,
98
+ sizeRange: [8, 32],
99
+ },
100
+ toolbar: {
101
+ row1: [
102
+ { id: 'esc', label: 'Esc', description: 'Send Escape key', action: { type: 'send', data: '\x1b' } },
103
+ { id: 'tmux-prefix', label: 'Prefix', description: 'Send tmux prefix key (Ctrl-B)', action: { type: 'send', data: '\x02' } },
104
+ // ...
105
+ ],
106
+ row2: [
107
+ { id: 'alt-enter', label: 'M-↵', description: 'Send Alt+Enter (ESC + Enter)', action: { type: 'send', data: '\x1b\r' } },
108
+ { id: 'drawer-toggle', label: '☰ More', description: 'Open command drawer', action: { type: 'drawer-toggle' } },
109
+ { id: 'paste', label: 'Paste', description: 'Paste from clipboard', action: { type: 'paste' } },
110
+ { id: 'backspace', label: '⌫', description: 'Send Backspace key', action: { type: 'send', data: '\x7f' } },
111
+ // ...
112
+ ],
113
+ },
114
+ drawer: {
115
+ buttons: [
116
+ { id: 'tmux-new-window', label: '+ Win', description: 'Create tmux window', action: { type: 'send', data: '\x02c' } },
117
+ { id: 'tmux-split-vertical', label: 'Split |', description: 'Split pane vertically', action: { type: 'send', data: '\x02%' } },
118
+ { id: 'combo-picker', label: 'Combo', description: 'Open combo sender (Ctrl/Alt + key)', action: { type: 'combo-picker' } },
119
+ // ...
120
+ ],
121
+ },
122
+ gestures: {
123
+ swipe: {
124
+ enabled: true,
125
+ left: '\x02n', // data sent on swipe left (default: next tmux window)
126
+ right: '\x02p', // data sent on swipe right (default: prev tmux window)
127
+ leftLabel: 'Next tmux window', // shown in help overlay
128
+ rightLabel: 'Previous tmux window',
129
+ },
130
+ scroll: {
131
+ enabled: true,
132
+ strategy: 'wheel',
133
+ sensitivity: 40,
134
+ wheelIntervalMs: 24,
135
+ },
136
+ pinch: { enabled: true },
137
+ },
138
+ mobile: {
139
+ initData: '\x02z', // send on mobile load when viewport < widthThreshold
140
+ widthThreshold: 768, // px — default matches common phone/tablet breakpoint
141
+ },
142
+ floatingButtons: [
143
+ {
144
+ position: 'top-left',
145
+ buttons: [
146
+ { id: 'zoom', label: 'Zoom', description: 'Toggle pane zoom', action: { type: 'send', data: '\x02z' } },
147
+ ],
148
+ },
149
+ ],
150
+ })
151
+ ```
152
+
153
+ <details>
154
+ <summary>Configure with an AI agent</summary>
155
+
156
+ ### Install the remobi skill
157
+
158
+ ```bash
159
+ npx skills add connorads/remobi
160
+ ```
161
+
162
+ For a specific agent only (e.g. Claude Code, globally): `npx skills add connorads/remobi -a claude-code -g`
163
+
164
+ ### Or paste this prompt
165
+
166
+ > Inspect my tmux config (`tmux show-options -g prefix` and `tmux list-keys`), then generate a `remobi.config.ts` tailored to my setup. Allowed root keys: `name theme font toolbar drawer gestures mobile floatingButtons pwa reconnect`. Action types: `send | ctrl-modifier | paste | combo-picker | drawer-toggle`. Use `drawer.buttons` not `drawer.commands`. Validate with `remobi build --dry-run` and fix any errors. Summarise what was configured.
167
+
168
+ See the full [agent setup guide](docs/guides/agent-setup.md) for examples and escape-code reference.
169
+
170
+ </details>
171
+
172
+ All fields are optional — defaults are filled in via `defineConfig()`.
173
+
174
+ Shipped tmux drawer defaults stick to stock tmux bindings (`c`, `%`, `"`, `s`, `w`, `[`, `?`, `x`, `z`) rather than personal popup workflows.
175
+
176
+ Replace the drawer entirely with a plain array when you want a fully custom setup:
177
+
178
+ ```typescript
179
+ import { defineConfig } from 'remobi/config'
180
+
181
+ export default defineConfig({
182
+ drawer: {
183
+ buttons: [
184
+ { id: 'sessions', label: 'Sessions', description: 'Choose tmux session', action: { type: 'send', data: '\x02s' } },
185
+ { id: 'git', label: 'Git', description: 'Open my tmux git popup', action: { type: 'send', data: '\x02g' } },
186
+ ],
187
+ },
188
+ })
189
+ ```
190
+
191
+ At runtime, remobi validates the config object shape and rejects unknown keys with clear path-based errors.
192
+
193
+ `gestures.scroll.strategy` controls touch scroll behaviour:
194
+
195
+ - `wheel` (default): sends SGR mouse wheel events with touch-mapped terminal coordinates.
196
+ - `keys`: sends `PageUp` / `PageDown` for app-level paging when preferred.
197
+
198
+ ### Programmatic API
199
+
200
+ ```typescript
201
+ import { defineConfig, serialiseThemeForTtyd } from 'remobi/config'
202
+ import type { RemobiConfig, ControlButton } from 'remobi/types'
203
+ import { init } from 'remobi'
204
+ ```
205
+
206
+ Advanced consumers can use hook registry primitives to observe lifecycle and terminal-send events:
207
+
208
+ ```typescript
209
+ import { createHookRegistry, init } from 'remobi'
210
+
211
+ const hooks = createHookRegistry()
212
+ hooks.on('beforeSendData', (ctx) => {
213
+ if (ctx.data.includes('rm -rf /')) return { block: true }
214
+ })
215
+
216
+ init(undefined, hooks)
217
+ ```
218
+
219
+ ## Deployment guides
220
+
221
+ - [Tailscale Serve](docs/guides/tailscale-serve.md) — expose over your tailnet with HTTPS
222
+ - [Keeping your Mac awake](docs/guides/keep-awake.md) — prevent sleep during remote sessions
223
+ - [ttyd flags](docs/guides/ttyd-flags.md) — recommended ttyd options and theme flags
224
+ - [Mobile pane navigation](docs/guides/mobile-panes.md) — zoom-aware swipe, auto-zoom on load, floating buttons
225
+ - [Mobile-friendly tmux config](docs/guides/mobile-tmux.md) — responsive status bar, popup sizing, binding ergonomics
226
+ - [Agent setup](docs/guides/agent-setup.md) — configure remobi with AI agents
227
+
228
+ ## Architecture
229
+
230
+ Pure TypeScript + DOM API — no framework. The build bundles all JS/CSS into a single HTML file via esbuild. ttyd handles WebSocket/PTY bridging; remobi only adds the mobile UI overlay.
231
+
232
+ Key modules:
233
+
234
+ | Module | Purpose |
235
+ |--------|---------|
236
+ | `src/toolbar/` | Two-row touch toolbar |
237
+ | `src/drawer/` | Command drawer with grid layout |
238
+ | `src/gestures/` | Swipe, pinch, scroll detection |
239
+ | `src/controls/` | Font size, help overlay, scroll buttons |
240
+ | `src/theme/` | Catppuccin Mocha + theme application |
241
+ | `src/viewport/` | Height management, landscape detection |
242
+ | `src/util/` | DOM helpers, terminal, keyboard, haptics |
243
+
244
+ ## Public API and semver
245
+
246
+ remobi follows semantic versioning. The public API is defined by the following import paths:
247
+
248
+ | Import path | Contents | Stability |
249
+ |---|---|---|
250
+ | `remobi` | `init`, `defineConfig`, `createHookRegistry`, `RemobiConfig`, `ControlButton`, `ButtonAction`, `ButtonArrayPatch`, `ButtonArrayInput`, `RemobiConfigOverrides`, `HookRegistry` | Public — breaking changes are semver-major |
251
+ | `remobi/config` | `defineConfig`, `mergeConfig`, `defaultConfig`, `serialiseThemeForTtyd` | Public |
252
+ | `remobi/types` | All types in `src/types.ts` | Public |
253
+
254
+ **Internal modules** (not part of the public API — may change without a major version bump):
255
+ `src/toolbar/`, `src/drawer/`, `src/gestures/`, `src/controls/`, `src/theme/`, `src/viewport/`, `src/util/`, `src/serve.ts`, `src/cli/`, `build.ts`
256
+
257
+ **Semver policy**:
258
+ - **Major**: removing or renaming a public export, changing a public function signature incompatibly, removing a config field
259
+ - **Minor**: adding new public exports, new optional config fields, new `ButtonArrayInput` operations
260
+ - **Patch**: bug fixes, internal refactors, documentation updates
261
+
262
+ ## Development
263
+
264
+ ```bash
265
+ pnpm install
266
+ pnpm test
267
+ pnpm run check # biome lint + format
268
+ pnpm run build # build dist/index.html (dev-time, uses tsx)
269
+ ```
270
+
271
+ For local development:
272
+
273
+ ```bash
274
+ cd ~/git/remobi
275
+ pnpm link --global # remobi CLI available globally
276
+ pnpm run build:dist # transpile TS → JS (tsdown)
277
+ ```
278
+
279
+ ## FAQ
280
+
281
+ **Is this secure?**
282
+ remobi doesn't handle auth — it's a UI overlay. Use a tunnel or VPN you trust. We recommend [Tailscale](docs/guides/tailscale-serve.md) (deployment guide included) — your session never leaves your tailnet. Cloudflare Tunnel and ngrok also work. Security is your responsibility.
283
+
284
+ **Why not Termux / Termius / SSH apps?**
285
+ They work. But you're managing SSH keys, losing your tmux setup, and fighting a UI that wasn't built for touch. remobi keeps your exact workflow — same panes, same windows, same bindings — and adds touch controls on top.
286
+
287
+ **Why not [Happy](https://github.com/slopus/happy) / Claude resume / chat-based mobile apps?**
288
+ Those tools change your workflow. Chat relays route through third-party servers. Claude's resume has limitations. remobi gives you the raw terminal — full power, self-hosted, works with every agent because it works with tmux.
289
+
290
+ **Why Node?**
291
+ remobi migrated from Bun to Node.js + pnpm for broader compatibility. It transpiles to JS via tsdown for npm distribution and uses esbuild for the browser overlay bundle.
292
+
293
+ **Is this production-ready?**
294
+ It's v0.1. The author uses it daily. It works. It's also early — feedback welcome, forks encouraged.
295
+
296
+ ## Acknowledgements
297
+
298
+ remobi is a thin overlay. The heavy lifting is done by [ttyd](https://github.com/tsl0922/ttyd) (terminal sharing over the web) and [xterm.js](https://xtermjs.org/) (terminal rendering in the browser). remobi just adds the mobile touch controls on top.
299
+
300
+ ## Licence
301
+
302
+ MIT
@@ -0,0 +1,17 @@
1
+ import { RemobiConfig } from "./src/types.mjs";
2
+
3
+ //#region build.d.ts
4
+ /** Bundle the overlay JS + CSS into strings */
5
+ declare function bundleOverlay(config: RemobiConfig): Promise<{
6
+ js: string;
7
+ css: string;
8
+ }>;
9
+ /** Inject remobi overlay into ttyd's HTML */
10
+ declare function injectOverlay(html: string, js: string, css: string, config: RemobiConfig): string;
11
+ /** Full build pipeline: bundle → fetch ttyd HTML → inject → write output */
12
+ declare function build(config: RemobiConfig, outputPath: string): Promise<void>;
13
+ /** Build from stdin HTML (pipe mode) */
14
+ declare function injectFromStdin(config: RemobiConfig): Promise<string>;
15
+ //#endregion
16
+ export { build, bundleOverlay, injectFromStdin, injectOverlay };
17
+ //# sourceMappingURL=build.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.mts","names":[],"sources":["../build.ts"],"mappings":";;;;iBAmBsB,aAAA,CAAc,MAAA,EAAQ,YAAA,GAAe,OAAA;EAAU,EAAA;EAAY,GAAA;AAAA;;iBA+FjE,aAAA,CAAc,IAAA,UAAc,EAAA,UAAY,GAAA,UAAa,MAAA,EAAQ,YAAA;;iBAoBvD,KAAA,CAAM,MAAA,EAAQ,YAAA,EAAc,UAAA,WAAqB,OAAA;;iBAQjD,eAAA,CAAgB,MAAA,EAAQ,YAAA,GAAe,OAAA"}
package/dist/build.mjs ADDED
@@ -0,0 +1,115 @@
1
+ import { i as generatePwaHtml, n as sleep, r as spawnProcess, t as readStdin } from "./node-compat-BzXgbTV9.mjs";
2
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ //#region build.ts
6
+ function findProjectRoot() {
7
+ let dir = import.meta.dirname;
8
+ for (let i = 0; i < 5; i++) {
9
+ if (existsSync(resolve(dir, "styles/base.css"))) return dir;
10
+ dir = dirname(dir);
11
+ }
12
+ return import.meta.dirname;
13
+ }
14
+ const PROJECT_ROOT = findProjectRoot();
15
+ /** Bundle the overlay JS + CSS into strings */
16
+ async function bundleOverlay(config) {
17
+ const css = readFileSync(resolve(PROJECT_ROOT, "styles/base.css"), "utf-8");
18
+ const prebuiltPath = resolve(PROJECT_ROOT, "dist/overlay.iife.js");
19
+ if (existsSync(prebuiltPath)) {
20
+ const overlayJs = readFileSync(prebuiltPath, "utf-8");
21
+ return {
22
+ js: `globalThis.__remobiConfig=${JSON.stringify(config)};${overlayJs}`,
23
+ css
24
+ };
25
+ }
26
+ const esbuild = await import("esbuild");
27
+ const entryCode = `
28
+ import { init, createHookRegistry } from './src/index.ts'
29
+ const hooks = createHookRegistry()
30
+ const config = ${JSON.stringify(config)}
31
+ ;(function() { init(config, hooks) })()
32
+ `;
33
+ const tmpEntry = resolve(PROJECT_ROOT, ".tmp-entry.ts");
34
+ writeFileSync(tmpEntry, entryCode);
35
+ try {
36
+ const output = (await esbuild.build({
37
+ entryPoints: [tmpEntry],
38
+ bundle: true,
39
+ platform: "browser",
40
+ minify: true,
41
+ format: "esm",
42
+ write: false
43
+ })).outputFiles[0];
44
+ if (!output) throw new Error("Build produced no output");
45
+ return {
46
+ js: output.text,
47
+ css
48
+ };
49
+ } finally {
50
+ try {
51
+ unlinkSync(tmpEntry);
52
+ } catch {}
53
+ }
54
+ }
55
+ /** Fetch ttyd's base index.html by starting a temporary instance */
56
+ async function fetchTtydHtml() {
57
+ const port = 19876 + Math.floor(Math.random() * 1e3);
58
+ const proc = spawnProcess([
59
+ "ttyd",
60
+ "--port",
61
+ String(port),
62
+ "-i",
63
+ "127.0.0.1",
64
+ "echo",
65
+ "noop"
66
+ ], {
67
+ stdout: "ignore",
68
+ stderr: "ignore"
69
+ });
70
+ let html = "";
71
+ for (let i = 0; i < 30; i++) {
72
+ await sleep(200);
73
+ try {
74
+ const resp = await fetch(`http://127.0.0.1:${port}/`);
75
+ if (resp.ok) {
76
+ html = await resp.text();
77
+ break;
78
+ }
79
+ } catch {}
80
+ }
81
+ proc.kill();
82
+ await proc.exited;
83
+ if (!html) throw new Error("Failed to fetch ttyd index.html — is ttyd installed and on PATH?\nInstall ttyd: macOS `brew install ttyd`; Linux use your distro package manager or build from source: https://github.com/tsl0922/ttyd#installation\nAlternatively, pipe existing ttyd HTML via `remobi inject` (no ttyd required).");
84
+ return html;
85
+ }
86
+ /** Synchronous script that captures WebSocket instances for reconnect detection */
87
+ const WS_INTERCEPTOR = "<script>(function(){var O=WebSocket,S=window.__remobiSockets=[];window.WebSocket=class extends O{constructor(u,p){super(u,p);S.push(this)}}})()<\/script>";
88
+ /** Inject remobi overlay into ttyd's HTML */
89
+ function injectOverlay(html, js, css, config) {
90
+ const fontLink = `<link rel="preload" href="${config.font.cdnUrl}" as="style" onload="this.rel='stylesheet'">`;
91
+ const viewport = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover\">";
92
+ const styleTag = `<style>${css}</style>`;
93
+ const scriptTag = `<script type="module">${js.replace(/<(?=\/script)/gi, "\\x3c")}<\/script>`;
94
+ const pwaHtml = config.pwa.enabled ? `${generatePwaHtml(config.name, config.pwa)}\n` : "";
95
+ const injection = `${config.reconnect.enabled ? `${WS_INTERCEPTOR}\n` : ""}${fontLink}\n${viewport}\n${pwaHtml}${styleTag}\n${scriptTag}\n`;
96
+ const idx = html.indexOf("</head>");
97
+ if (idx === -1) throw new Error("No </head> found in base HTML");
98
+ return html.slice(0, idx) + injection + html.slice(idx);
99
+ }
100
+ /** Full build pipeline: bundle → fetch ttyd HTML → inject → write output */
101
+ async function build(config, outputPath) {
102
+ const { js, css } = await bundleOverlay(config);
103
+ writeFileSync(outputPath, injectOverlay(await fetchTtydHtml(), js, css, config));
104
+ }
105
+ /** Build from stdin HTML (pipe mode) */
106
+ async function injectFromStdin(config) {
107
+ const { js, css } = await bundleOverlay(config);
108
+ const stdin = await readStdin();
109
+ if (stdin.trim().length === 0) throw new Error("remobi inject expects piped ttyd HTML on stdin");
110
+ return injectOverlay(stdin, js, css, config);
111
+ }
112
+
113
+ //#endregion
114
+ export { build, bundleOverlay, injectFromStdin, injectOverlay };
115
+ //# sourceMappingURL=build.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.mjs","names":[],"sources":["../build.ts"],"sourcesContent":["import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { generatePwaHtml } from './src/pwa/meta-tags'\nimport type { RemobiConfig } from './src/types'\nimport { readStdin, sleep, spawnProcess } from './src/util/node-compat'\n\n// Walk up from module location to find project root (where styles/ lives)\nfunction findProjectRoot(): string {\n\tlet dir = import.meta.dirname\n\tfor (let i = 0; i < 5; i++) {\n\t\tif (existsSync(resolve(dir, 'styles/base.css'))) return dir\n\t\tdir = dirname(dir)\n\t}\n\treturn import.meta.dirname\n}\n\nconst PROJECT_ROOT = findProjectRoot()\n\n/** Bundle the overlay JS + CSS into strings */\nexport async function bundleOverlay(config: RemobiConfig): Promise<{ js: string; css: string }> {\n\t// Read CSS\n\tconst cssPath = resolve(PROJECT_ROOT, 'styles/base.css')\n\tconst css = readFileSync(cssPath, 'utf-8')\n\n\t// Pre-built overlay: read dist/overlay.iife.js and prepend config via globalThis\n\tconst prebuiltPath = resolve(PROJECT_ROOT, 'dist/overlay.iife.js')\n\tif (existsSync(prebuiltPath)) {\n\t\tconst overlayJs = readFileSync(prebuiltPath, 'utf-8')\n\t\tconst js = `globalThis.__remobiConfig=${JSON.stringify(config)};${overlayJs}`\n\t\treturn { js, css }\n\t}\n\n\t// Dev fallback: bundle from source via esbuild (requires src/ and esbuild)\n\tconst esbuild = await import('esbuild')\n\n\tconst configJson = JSON.stringify(config)\n\tconst entryCode = `\nimport { init, createHookRegistry } from './src/index.ts'\nconst hooks = createHookRegistry()\nconst config = ${configJson}\n;(function() { init(config, hooks) })()\n`\n\n\tconst tmpEntry = resolve(PROJECT_ROOT, '.tmp-entry.ts')\n\twriteFileSync(tmpEntry, entryCode)\n\n\ttry {\n\t\tconst result = await esbuild.build({\n\t\t\tentryPoints: [tmpEntry],\n\t\t\tbundle: true,\n\t\t\tplatform: 'browser',\n\t\t\tminify: true,\n\t\t\tformat: 'esm',\n\t\t\twrite: false,\n\t\t})\n\n\t\tconst output = result.outputFiles[0]\n\t\tif (!output) {\n\t\t\tthrow new Error('Build produced no output')\n\t\t}\n\t\tconst js = output.text\n\n\t\treturn { js, css }\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(tmpEntry)\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n}\n\n/** Fetch ttyd's base index.html by starting a temporary instance */\nasync function fetchTtydHtml(): Promise<string> {\n\tconst port = 19876 + Math.floor(Math.random() * 1000)\n\tconst proc = spawnProcess(['ttyd', '--port', String(port), '-i', '127.0.0.1', 'echo', 'noop'], {\n\t\tstdout: 'ignore',\n\t\tstderr: 'ignore',\n\t})\n\n\t// Wait for ttyd to start\n\tlet html = ''\n\tfor (let i = 0; i < 30; i++) {\n\t\tawait sleep(200)\n\t\ttry {\n\t\t\tconst resp = await fetch(`http://127.0.0.1:${port}/`)\n\t\t\tif (resp.ok) {\n\t\t\t\thtml = await resp.text()\n\t\t\t\tbreak\n\t\t\t}\n\t\t} catch {\n\t\t\t// not ready yet\n\t\t}\n\t}\n\n\tproc.kill()\n\tawait proc.exited\n\n\tif (!html) {\n\t\tthrow new Error(\n\t\t\t'Failed to fetch ttyd index.html — is ttyd installed and on PATH?\\n' +\n\t\t\t\t'Install ttyd: macOS `brew install ttyd`; Linux use your distro package manager or build from source: https://github.com/tsl0922/ttyd#installation\\n' +\n\t\t\t\t'Alternatively, pipe existing ttyd HTML via `remobi inject` (no ttyd required).',\n\t\t)\n\t}\n\n\treturn html\n}\n\n/** Synchronous script that captures WebSocket instances for reconnect detection */\nconst WS_INTERCEPTOR =\n\t'<script>(function(){var O=WebSocket,S=window.__remobiSockets=[];window.WebSocket=class extends O{constructor(u,p){super(u,p);S.push(this)}}})()</script>'\n\n/** Inject remobi overlay into ttyd's HTML */\nexport function injectOverlay(html: string, js: string, css: string, config: RemobiConfig): string {\n\tconst fontLink = `<link rel=\"preload\" href=\"${config.font.cdnUrl}\" as=\"style\" onload=\"this.rel='stylesheet'\">`\n\tconst viewport =\n\t\t'<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover\">'\n\tconst styleTag = `<style>${css}</style>`\n\tconst safeJs = js.replace(/<(?=\\/script)/gi, '\\\\x3c')\n\tconst scriptTag = `<script type=\"module\">${safeJs}</script>`\n\tconst pwaHtml = config.pwa.enabled ? `${generatePwaHtml(config.name, config.pwa)}\\n` : ''\n\tconst wsScript = config.reconnect.enabled ? `${WS_INTERCEPTOR}\\n` : ''\n\n\tconst injection = `${wsScript}${fontLink}\\n${viewport}\\n${pwaHtml}${styleTag}\\n${scriptTag}\\n`\n\n\t// Avoid String.replace() — minified JS may contain $& which .replace()\n\t// interprets as a special replacement pattern, corrupting the output\n\tconst idx = html.indexOf('</head>')\n\tif (idx === -1) throw new Error('No </head> found in base HTML')\n\treturn html.slice(0, idx) + injection + html.slice(idx)\n}\n\n/** Full build pipeline: bundle → fetch ttyd HTML → inject → write output */\nexport async function build(config: RemobiConfig, outputPath: string): Promise<void> {\n\tconst { js, css } = await bundleOverlay(config)\n\tconst baseHtml = await fetchTtydHtml()\n\tconst patched = injectOverlay(baseHtml, js, css, config)\n\twriteFileSync(outputPath, patched)\n}\n\n/** Build from stdin HTML (pipe mode) */\nexport async function injectFromStdin(config: RemobiConfig): Promise<string> {\n\tconst { js, css } = await bundleOverlay(config)\n\tconst stdin = await readStdin()\n\tif (stdin.trim().length === 0) {\n\t\tthrow new Error('remobi inject expects piped ttyd HTML on stdin')\n\t}\n\treturn injectOverlay(stdin, js, css, config)\n}\n"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ //#region src/config-resolve.ts
2
+ /** Type predicate — Array.isArray doesn't narrow `readonly T[]` from generic unions */
3
+ function isReadonlyArray(input) {
4
+ return Array.isArray(input);
5
+ }
6
+ /**
7
+ * Resolve a ButtonArrayInput against the defaults array.
8
+ *
9
+ * - undefined → return defaults unchanged
10
+ * - Array → return as-is (replaces defaults entirely)
11
+ * - Function → call with defaults, return result
12
+ */
13
+ function resolveButtonArray(defaults, input) {
14
+ if (input === void 0) return defaults;
15
+ if (isReadonlyArray(input)) return input;
16
+ return input(defaults);
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/theme/catppuccin-mocha.ts
21
+ /** Catppuccin Mocha colour palette for xterm.js */
22
+ const catppuccinMocha = {
23
+ background: "#1e1e2e",
24
+ foreground: "#cdd6f4",
25
+ cursor: "#f5e0dc",
26
+ cursorAccent: "#1e1e2e",
27
+ selectionBackground: "#45475a",
28
+ black: "#45475a",
29
+ red: "#f38ba8",
30
+ green: "#a6e3a1",
31
+ yellow: "#f9e2af",
32
+ blue: "#89b4fa",
33
+ magenta: "#cba6f7",
34
+ cyan: "#94e2d5",
35
+ white: "#bac2de",
36
+ brightBlack: "#585b70",
37
+ brightRed: "#f38ba8",
38
+ brightGreen: "#a6e3a1",
39
+ brightYellow: "#f9e2af",
40
+ brightBlue: "#89b4fa",
41
+ brightMagenta: "#cba6f7",
42
+ brightCyan: "#94e2d5",
43
+ brightWhite: "#a6adc8"
44
+ };
45
+
46
+ //#endregion
47
+ export { resolveButtonArray as n, catppuccinMocha as t };
48
+ //# sourceMappingURL=catppuccin-mocha-CGTshAuT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catppuccin-mocha-CGTshAuT.mjs","names":[],"sources":["../src/config-resolve.ts","../src/theme/catppuccin-mocha.ts"],"sourcesContent":["import type { ButtonArrayInput } from './types'\n\n/** Type predicate — Array.isArray doesn't narrow `readonly T[]` from generic unions */\nfunction isReadonlyArray<T extends { readonly id: string }>(\n\tinput: ButtonArrayInput<T>,\n): input is readonly T[] {\n\treturn Array.isArray(input)\n}\n\n/**\n * Resolve a ButtonArrayInput against the defaults array.\n *\n * - undefined → return defaults unchanged\n * - Array → return as-is (replaces defaults entirely)\n * - Function → call with defaults, return result\n */\nexport function resolveButtonArray<T extends { readonly id: string }>(\n\tdefaults: readonly T[],\n\tinput: ButtonArrayInput<T> | undefined,\n): readonly T[] {\n\tif (input === undefined) {\n\t\treturn defaults\n\t}\n\n\tif (isReadonlyArray(input)) {\n\t\treturn input\n\t}\n\n\treturn input(defaults)\n}\n","import type { TermTheme } from '../types'\n\n/** Catppuccin Mocha colour palette for xterm.js */\nexport const catppuccinMocha: TermTheme = {\n\tbackground: '#1e1e2e',\n\tforeground: '#cdd6f4',\n\tcursor: '#f5e0dc',\n\tcursorAccent: '#1e1e2e',\n\tselectionBackground: '#45475a',\n\tblack: '#45475a',\n\tred: '#f38ba8',\n\tgreen: '#a6e3a1',\n\tyellow: '#f9e2af',\n\tblue: '#89b4fa',\n\tmagenta: '#cba6f7',\n\tcyan: '#94e2d5',\n\twhite: '#bac2de',\n\tbrightBlack: '#585b70',\n\tbrightRed: '#f38ba8',\n\tbrightGreen: '#a6e3a1',\n\tbrightYellow: '#f9e2af',\n\tbrightBlue: '#89b4fa',\n\tbrightMagenta: '#cba6f7',\n\tbrightCyan: '#94e2d5',\n\tbrightWhite: '#a6adc8',\n}\n"],"mappings":";;AAGA,SAAS,gBACR,OACwB;AACxB,QAAO,MAAM,QAAQ,MAAM;;;;;;;;;AAU5B,SAAgB,mBACf,UACA,OACe;AACf,KAAI,UAAU,OACb,QAAO;AAGR,KAAI,gBAAgB,MAAM,CACzB,QAAO;AAGR,QAAO,MAAM,SAAS;;;;;;ACzBvB,MAAa,kBAA6B;CACzC,YAAY;CACZ,YAAY;CACZ,QAAQ;CACR,cAAc;CACd,qBAAqB;CACrB,OAAO;CACP,KAAK;CACL,OAAO;CACP,QAAQ;CACR,MAAM;CACN,SAAS;CACT,MAAM;CACN,OAAO;CACP,aAAa;CACb,WAAW;CACX,aAAa;CACb,cAAc;CACd,YAAY;CACZ,eAAe;CACf,YAAY;CACZ,aAAa;CACb"}
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };