Skip to content

Commit 816dcb6

Browse files
committed
fix(cli): scaffold portable configs and support base paths
Generate plain default-exported configs so global installs do not depend on resolving remobi from the project directory. Add --base-path for prefixed deployments and thread it through the browser, PWA assets, manifest URLs, and server routes.
1 parent 8343885 commit 816dcb6

13 files changed

Lines changed: 372 additions & 148 deletions

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ See [Mobile-friendly tmux config](.agents/skills/remobi-setup/references/mobile-
6868

6969
For local development, see the [Development](#development) section below.
7070

71-
Open `http://localhost:7681` on the same machine to verify it works. For phone access, put a trusted proxy/tunnel in front of it, for example [Tailscale Serve](.agents/skills/remobi-setup/references/tailscale-serve.md).
71+
Open `http://localhost:7681` on the same machine to verify it works. For phone access, put a trusted proxy/tunnel in front of it, for example [Tailscale Serve](.agents/skills/remobi-setup/references/tailscale-serve.md). If your proxy mounts remobi under a URL prefix, start remobi with `--base-path /that-prefix` so the HTML, PWA links, and WebSocket all use the same external path.
7272

7373
## Release channels
7474

@@ -117,10 +117,11 @@ To report a vulnerability, see [SECURITY.md](SECURITY.md).
117117
## CLI reference
118118

119119
```text
120-
remobi serve [--config <path>] [--port <n>] [--host <addr>] [-- <command...>]
120+
remobi serve [--config <path>] [--port <n>] [--host <addr>] [--base-path <path>] [-- <command...>]
121121
Start remobi with its built-in web terminal and PWA support.
122122
Default host: 127.0.0.1. Default port: 7681. Default command: tmux new-session -A -s main
123123
Example: remobi serve --host 0.0.0.0 --port 8080
124+
Example: remobi serve --base-path /random-token
124125
Example: remobi serve --port 8080 -- tmux new -As dev
125126
126127
remobi build [--config <path>] [--output <path>] [--dry-run]
@@ -150,9 +151,7 @@ When `--config` is not specified, remobi searches:
150151
Create `remobi.config.ts` (or run `remobi init`):
151152

152153
```typescript
153-
import { defineConfig } from 'remobi/config'
154-
155-
export default defineConfig({
154+
export default {
156155
font: {
157156
family: 'JetBrainsMono NFM, monospace',
158157
mobileSizeDefault: 16,
@@ -208,10 +207,10 @@ export default defineConfig({
208207
],
209208
},
210209
],
211-
})
210+
}
212211
```
213212

214-
All fields are optional — defaults are filled in via `defineConfig()`.
213+
All fields are optional — the CLI fills in defaults internally when it loads the config.
215214

216215
Shipped tmux drawer defaults stick to stock tmux bindings (`c`, `%`, `"`, `s`, `w`, `[`, `?`, `x`, `z`) rather than personal popup workflows.
217216

build.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ function readPrebuiltAsset(filename: string): string | null {
5050
export async function bundleClientAssets(
5151
config: RemobiConfig,
5252
version: string,
53+
basePath = '/',
5354
): Promise<{ js: string; css: string }> {
5455
const prebuiltJs = readPrebuiltAsset('client.iife.js')
5556
const prebuiltCss = readPrebuiltAsset('client.css')
5657
if (prebuiltJs !== null && prebuiltCss !== null) {
57-
const js = `globalThis.__remobiVersion=${JSON.stringify(version)};globalThis.__remobiConfig=${JSON.stringify(config)};${prebuiltJs}`
58+
const js = `globalThis.__remobiVersion=${JSON.stringify(version)};globalThis.__remobiConfig=${JSON.stringify(config)};globalThis.__remobiBasePath=${JSON.stringify(basePath)};${prebuiltJs}`
5859
return { js, css: prebuiltCss }
5960
}
6061

@@ -78,7 +79,7 @@ export async function bundleClientAssets(
7879
throw new Error('remobi client build produced incomplete output')
7980
}
8081

81-
const js = `globalThis.__remobiVersion=${JSON.stringify(version)};globalThis.__remobiConfig=${JSON.stringify(config)};${jsOutput.text}`
82+
const js = `globalThis.__remobiVersion=${JSON.stringify(version)};globalThis.__remobiConfig=${JSON.stringify(config)};globalThis.__remobiBasePath=${JSON.stringify(basePath)};${jsOutput.text}`
8283
return { js, css: cssOutput.text }
8384
}
8485

@@ -87,10 +88,14 @@ export function renderClientHtml(
8788
css: string,
8889
config: RemobiConfig,
8990
scriptNonce: string,
91+
basePath = '/',
9092
): string {
9193
const viewport =
9294
'<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover">'
93-
const pwaHtml = config.pwa.enabled ? `${generatePwaHtml(config.name, config.pwa)}\n` : ''
95+
const pwaHtml = config.pwa.enabled
96+
? `${generatePwaHtml(config.name, config.pwa, basePath)}
97+
`
98+
: ''
9499
const fontLink = `<link rel="stylesheet" href="${escapeAttr(config.font.cdnUrl)}">`
95100
const safeJs = js.replace(/<(?=\/script)/gi, '\\x3c')
96101

cli.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function usage(): void {
5757
console.log(`remobi v${VERSION} — mobile terminal overlay for tmux
5858
5959
Usage:
60-
remobi serve [--config <path>] [--port <n>] [--host <addr>] [--no-sleep] [-- <command...>]
60+
remobi serve [--config <path>] [--port <n>] [--host <addr>] [--base-path <path>] [--no-sleep] [-- <command...>]
6161
Start remobi with a built-in web terminal, PWA support, and the configured command.
6262
Default host: 127.0.0.1. Default port: 7681. Default command: tmux new-session -A -s main
6363
@@ -81,13 +81,15 @@ Flags:
8181
-o, --output <path> Deprecated build output path flag
8282
-p, --port <n> Port to serve on (serve only, default 7681)
8383
--host <addr> Host/interface to bind (serve only, default 127.0.0.1)
84+
--base-path <p> Mount remobi under a URL prefix such as /random-token
8485
-n, --dry-run Deprecated build/inject dry-run flag
8586
--no-sleep Prevent macOS sleep while serving (caffeinate -s, serve only)
8687
8788
Examples:
8889
remobi serve
8990
remobi serve --no-sleep
9091
remobi serve --host 0.0.0.0 --port 8080
92+
remobi serve --base-path /random-token
9193
remobi serve --port 8080 -- tmux new -As dev`)
9294
}
9395

@@ -225,7 +227,7 @@ async function main(): Promise<void> {
225227
process.exit(1)
226228
}
227229

228-
const { command, configPath, port, host, noSleep, command_ } = parsed.value
230+
const { command, configPath, port, host, basePath, noSleep, command_ } = parsed.value
229231

230232
switch (command) {
231233
case 'serve': {
@@ -237,6 +239,7 @@ async function main(): Promise<void> {
237239
noSleep,
238240
host,
239241
VERSION,
242+
basePath,
240243
)
241244
break
242245
}
@@ -255,9 +258,7 @@ async function main(): Promise<void> {
255258
console.error('remobi.config.ts already exists')
256259
process.exit(1)
257260
}
258-
const template = `import { defineConfig } from 'remobi'
259-
260-
export default defineConfig({
261+
const template = `export default {
261262
// name: 'remobi', // app name (tab title, PWA home screen label)
262263
// theme: 'catppuccin-mocha',
263264
// font: {
@@ -314,7 +315,7 @@ export default defineConfig({
314315
// reconnect: {
315316
// enabled: true, // show overlay + auto-reload on connection loss (default true)
316317
// },
317-
})
318+
}
318319
`
319320
writeFileSync(targetPath, template)
320321
console.log(`Created: ${targetPath}`)

src/base-path.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export function normalizeBasePath(value: string): string | null {
2+
if (value.length === 0 || !value.startsWith('/')) {
3+
return null
4+
}
5+
6+
if (value.includes('?') || value.includes('#')) {
7+
return null
8+
}
9+
10+
if (value === '/') {
11+
return '/'
12+
}
13+
14+
const trimmed = value.replace(/\/+$/g, '')
15+
if (trimmed.length === 0 || trimmed === '/') {
16+
return '/'
17+
}
18+
19+
return trimmed
20+
}
21+
22+
export function joinBasePath(basePath: string, path: string): string {
23+
if (basePath === '/') {
24+
return path
25+
}
26+
27+
if (path === '/') {
28+
return `${basePath}/`
29+
}
30+
31+
return `${basePath}${path}`
32+
}
33+
34+
export function documentRoute(basePath: string): string {
35+
return basePath === '/' ? '/' : `${basePath}/`
36+
}

src/cli/args.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { normalizeBasePath } from '../base-path'
2+
13
type CliCommand = 'build' | 'inject' | 'init' | 'serve' | 'help' | 'version'
24

35
interface ParsedCliArgs {
@@ -7,6 +9,7 @@ interface ParsedCliArgs {
79
readonly dryRun: boolean
810
readonly port?: number
911
readonly host?: string
12+
readonly basePath?: string
1013
readonly noSleep: boolean
1114
readonly command_: readonly string[]
1215
}
@@ -29,6 +32,7 @@ interface ParseState {
2932
dryRun: boolean
3033
port?: number
3134
host?: string
35+
basePath?: string
3236
noSleep: boolean
3337
}
3438

@@ -101,6 +105,19 @@ const flagDefs: readonly FlagDef[] = [
101105
return undefined
102106
},
103107
},
108+
{
109+
names: ['--base-path'],
110+
validCommands: ['serve'],
111+
takesValue: true,
112+
apply(value, state) {
113+
const normalized = normalizeBasePath(value ?? '')
114+
if (normalized === null) {
115+
return `Invalid base path: ${value}`
116+
}
117+
state.basePath = normalized
118+
return undefined
119+
},
120+
},
104121
{
105122
names: ['--no-sleep'],
106123
validCommands: ['serve'],
@@ -212,6 +229,7 @@ export function parseCliArgs(args: readonly string[]): ParseCliResult {
212229
dryRun: state.dryRun,
213230
port: state.port,
214231
host: state.host,
232+
basePath: state.basePath,
215233
noSleep: state.noSleep,
216234
command_: trailingCommand,
217235
},

src/client-entry.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { WebLinksAddon } from '@xterm/addon-web-links'
44
import { Terminal } from '@xterm/xterm'
55
import '@xterm/xterm/css/xterm.css'
66
import '../styles/base.css'
7+
import { joinBasePath } from './base-path'
78
import { createHookRegistry, init } from './index'
89
import { parseServerMessage, serialiseClientMessage } from './session-protocol'
910
import type { ClientMessage } from './session-protocol'
@@ -13,10 +14,12 @@ import { onTap } from './util/tap'
1314

1415
declare const __remobiConfig: RemobiConfig
1516
declare const __remobiVersion: string | undefined
17+
declare const __remobiBasePath: string | undefined
1618

1719
function createSocketUrl(): string {
1820
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
19-
return `${protocol}//${window.location.host}/ws`
21+
const socketPath = joinBasePath(__remobiBasePath ?? '/', '/ws')
22+
return `${protocol}//${window.location.host}${socketPath}`
2023
}
2124

2225
function attachOptionalAddons(term: Terminal): FitAddon {

src/pwa/manifest.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { documentRoute, joinBasePath } from '../base-path'
12
import type { PwaConfig } from '../types'
23

34
interface WebAppManifest {
@@ -16,22 +17,27 @@ interface WebAppManifest {
1617
}
1718

1819
/** Generate a web app manifest object from pwa config */
19-
export function generateManifest(name: string, pwa: PwaConfig): WebAppManifest {
20+
export function generateManifest(name: string, pwa: PwaConfig, basePath = '/'): WebAppManifest {
2021
return {
2122
name,
2223
short_name: pwa.shortName ?? name,
23-
start_url: '/',
24+
start_url: documentRoute(basePath),
2425
display: 'standalone',
2526
background_color: pwa.themeColor,
2627
theme_color: pwa.themeColor,
2728
icons: [
28-
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any maskable' },
29-
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
29+
{
30+
src: joinBasePath(basePath, '/icon-192.png'),
31+
sizes: '192x192',
32+
type: 'image/png',
33+
purpose: 'any maskable',
34+
},
35+
{ src: joinBasePath(basePath, '/icon-512.png'), sizes: '512x512', type: 'image/png' },
3036
],
3137
}
3238
}
3339

3440
/** Serialise manifest to JSON string */
35-
export function manifestToJson(name: string, pwa: PwaConfig): string {
36-
return JSON.stringify(generateManifest(name, pwa), null, 2)
41+
export function manifestToJson(name: string, pwa: PwaConfig, basePath = '/'): string {
42+
return JSON.stringify(generateManifest(name, pwa, basePath), null, 2)
3743
}

src/pwa/meta-tags.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { joinBasePath } from '../base-path'
12
import type { PwaConfig } from '../types'
23
import { ICON_SVG, svgToDataUri } from './icon'
34

@@ -11,14 +12,14 @@ export function escapeAttr(value: string): string {
1112
}
1213

1314
/** Generate PWA HTML to inject into </head> */
14-
export function generatePwaHtml(name: string, pwa: PwaConfig): string {
15+
export function generatePwaHtml(name: string, pwa: PwaConfig, basePath = '/'): string {
1516
const svgDataUri = svgToDataUri(ICON_SVG)
1617
return [
17-
`<link rel="manifest" href="/manifest.json">`,
18+
`<link rel="manifest" href="${escapeAttr(joinBasePath(basePath, '/manifest.json'))}">`,
1819
`<meta name="theme-color" content="${escapeAttr(pwa.themeColor)}">`,
19-
`<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">`,
20+
'<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">',
2021
`<meta name="apple-mobile-web-app-title" content="${escapeAttr(name)}">`,
21-
`<link rel="apple-touch-icon" href="/apple-touch-icon.png">`,
22+
`<link rel="apple-touch-icon" href="${escapeAttr(joinBasePath(basePath, '/apple-touch-icon.png'))}">`,
2223
`<link rel="icon" type="image/svg+xml" href="${svgDataUri}">`,
2324
].join('\n')
2425
}

0 commit comments

Comments
 (0)