ocremote 2.0.1 → 2.1.1
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 +9 -33
- package/lib/rpc.mjs +22 -13
- package/oc-remote.mjs +10 -84
- package/package.json +2 -3
- package/install.sh +0 -262
package/README.md
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
# oc-remote
|
|
2
2
|
|
|
3
3
|
> **Plataformas.** El companion requiere Node >= 22.13 y soporta macOS, Linux y
|
|
4
|
-
> Windows: `npx ocremote --pair` en cualquiera de los tres.
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
> systemd ejecutando `npx ocremote --pair`; en Windows, una tarea del Programador
|
|
8
|
-
> de tareas al iniciar sesión. Los túneles (`--tunnel cloudflare`) descargan
|
|
4
|
+
> Windows: `npx ocremote --pair` en cualquiera de los tres. El proceso se mantiene
|
|
5
|
+
> en primer plano y la terminal debe seguir abierta. En macOS, mDNS está activo por
|
|
6
|
+
> defecto (`--no-mdns` lo desactiva; en otros sistemas no se intenta). Los túneles (`--tunnel cloudflare`) descargan
|
|
9
7
|
> `cloudflared` para la plataforma correcta. Windows está corregido en código
|
|
10
8
|
> (rutas, `.exe`, `;` en PATH, cloudflared .exe) pero no verificado en una máquina
|
|
11
9
|
> Windows real.
|
|
12
10
|
|
|
13
|
-
## Primer arranque (2.
|
|
11
|
+
## Primer arranque (2.1.1)
|
|
14
12
|
|
|
15
13
|
```bash
|
|
16
14
|
npm install -g opencode-ai
|
|
@@ -22,12 +20,11 @@ npx ocremote --pair
|
|
|
22
20
|
de red local. El proxy queda en loopback. El QR sólo se muestra cuando la invitación
|
|
23
21
|
está registrada: abre la app, escanéalo y conserva la terminal abierta mientras la
|
|
24
22
|
usas. Caduca en 15 minutos y es de un solo uso. Para mostrar otro QR de una instancia
|
|
25
|
-
en marcha: `npx ocremote pair`.
|
|
23
|
+
en marcha: `npx ocremote pair`.
|
|
26
24
|
|
|
27
25
|
La nueva app comprueba la firma del ordenador contra la clave fijada por el QR. Es
|
|
28
26
|
necesario actualizar app, companion y relay juntos y volver a emparejar las entradas
|
|
29
|
-
antiguas que no tengan esa clave.
|
|
30
|
-
paquete; este repositorio prepara 2.0.0.
|
|
27
|
+
antiguas que no tengan esa clave.
|
|
31
28
|
|
|
32
29
|
Las secciones de proxy/túneles de abajo describen también el modo directo opcional.
|
|
33
30
|
Con `--relay` o `--pair`, el valor predeterminado de `--host` es `127.0.0.1`, el túnel
|
|
@@ -100,11 +97,9 @@ para tests).
|
|
|
100
97
|
|
|
101
98
|
| Comando | Qué hace |
|
|
102
99
|
|---|---|
|
|
103
|
-
| `oc-remote doctor` | Verifica toda la cadena: binario de opencode, token, puerto, auth, upstream,
|
|
104
|
-
| `oc-remote status` | Config,
|
|
105
|
-
| `oc-remote
|
|
106
|
-
| `oc-remote logs` | Ruta del log y últimas 40 líneas |
|
|
107
|
-
| `oc-remote funnel on\|off\|status` | Activa/desactiva el Funnel de Tailscale (la primera vez imprime el link de aprobación). El daemon en `auto` lo adopta solo |
|
|
100
|
+
| `oc-remote doctor` | Verifica toda la cadena: binario de opencode, token, puerto, auth, upstream, LAN, Tailscale/Funnel, endpoint público y ntfy. Salida `ok`/`FAIL` con el arreglo de cada fallo |
|
|
101
|
+
| `oc-remote status` | Config, endpoint actual (de `endpoint.json`) y prueba de auth |
|
|
102
|
+
| `oc-remote funnel on\|off\|status` | Activa/desactiva el Funnel de Tailscale (la primera vez imprime el link de aprobación). El proceso en primer plano lo adopta automáticamente |
|
|
108
103
|
| `oc-remote version` | Versión |
|
|
109
104
|
|
|
110
105
|
## Beacons de endpoint (auto-reparación)
|
|
@@ -249,25 +244,6 @@ node companion/oc-remote.mjs --ntfy ocremote-raul-a8f3k2
|
|
|
249
244
|
`--ntfy https://ntfy.midominio.dev/mi-topic`.
|
|
250
245
|
- Los topics de ntfy.sh son publicos: usa uno largo y aleatorio.
|
|
251
246
|
|
|
252
|
-
## Instalacion como LaunchAgent
|
|
253
|
-
|
|
254
|
-
```bash
|
|
255
|
-
bash companion/install.sh --dir /Users/raul/Desktop/Projects/opencode-remote
|
|
256
|
-
bash companion/install.sh --port 4190 --token mi-token # idempotente
|
|
257
|
-
bash companion/install.sh --uninstall
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
Crea:
|
|
261
|
-
|
|
262
|
-
- symlink `~/.local/bin/oc-remote` → `companion/oc-remote.mjs`
|
|
263
|
-
- `~/Library/LaunchAgents/com.raul.ocremote.plist` (`KeepAlive`, `RunAtLoad`,
|
|
264
|
-
`WorkingDirectory`, `ThrottleInterval` 10 s, permisos 0600)
|
|
265
|
-
- logs en `~/Library/Logs/oc-remote.log` (stdout y stderr)
|
|
266
|
-
|
|
267
|
-
El QR y el token quedan en el log: `tail -f ~/Library/Logs/oc-remote.log`.
|
|
268
|
-
`--no-load` genera los ficheros sin cargar el agente (util para inspeccionar
|
|
269
|
-
el plist). No borra `~/.config/oc-remote/config.json` al desinstalar.
|
|
270
|
-
|
|
271
247
|
## Seguridad
|
|
272
248
|
|
|
273
249
|
- Token de 32 hex comparado con `crypto.timingSafeEqual`; el config se escribe
|
package/lib/rpc.mjs
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { b64 } from './crypto.mjs'
|
|
2
2
|
|
|
3
|
+
function sendChunks(session, id, buffer, size) {
|
|
4
|
+
for (let offset = 0; offset < buffer.length; offset += size) {
|
|
5
|
+
session.sendSealed({ t: 'chunk', id, data: b64(buffer.subarray(offset, offset + size)) })
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
const MAX_CONCURRENT = 8
|
|
4
10
|
const MAX_UPLOAD = 24 << 20
|
|
5
|
-
const MAX_RESPONSE =
|
|
11
|
+
const MAX_RESPONSE = 64 << 20
|
|
12
|
+
const SMALL_RESPONSE = 256 << 10
|
|
13
|
+
const RESPONSE_CHUNK = 256 << 10
|
|
14
|
+
const STREAM_CHUNK = 64 << 10
|
|
6
15
|
|
|
7
16
|
export class RpcAdapter {
|
|
8
17
|
constructor({ baseUrl, authHeader, logger }) {
|
|
@@ -92,6 +101,7 @@ export class RpcAdapter {
|
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
const res = await fetch(url, init)
|
|
104
|
+
const contentType = res.headers.get('content-type') || ''
|
|
95
105
|
if (!stream) {
|
|
96
106
|
const chunks = []
|
|
97
107
|
let size = 0
|
|
@@ -99,33 +109,32 @@ export class RpcAdapter {
|
|
|
99
109
|
size += chunk.length
|
|
100
110
|
if (size > MAX_RESPONSE) {
|
|
101
111
|
controller.abort()
|
|
102
|
-
session.sendSealed({ t: 'res', id, error: '
|
|
112
|
+
session.sendSealed({ t: 'res', id, error: 'response_too_large' })
|
|
103
113
|
return
|
|
104
114
|
}
|
|
105
115
|
chunks.push(Buffer.from(chunk))
|
|
106
116
|
}
|
|
107
117
|
const buffer = Buffer.concat(chunks)
|
|
108
|
-
|
|
109
|
-
t: 'res',
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
if (buffer.length <= SMALL_RESPONSE) {
|
|
119
|
+
session.sendSealed({ t: 'res', id, status: res.status, contentType, body: b64(buffer) })
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
opened = true
|
|
123
|
+
session.sendSealed({ t: 'open', id, status: res.status, contentType })
|
|
124
|
+
sendChunks(session, id, buffer, RESPONSE_CHUNK)
|
|
125
|
+
session.sendSealed({ t: 'end', id })
|
|
115
126
|
return
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
opened = true
|
|
119
|
-
session.sendSealed({ t: 'open', id, status: res.status, contentType
|
|
130
|
+
session.sendSealed({ t: 'open', id, status: res.status, contentType })
|
|
120
131
|
if (!res.body) {
|
|
121
132
|
session.sendSealed({ t: 'end', id })
|
|
122
133
|
return
|
|
123
134
|
}
|
|
124
135
|
for await (const chunk of res.body) {
|
|
125
136
|
if (controller.signal.aborted) break
|
|
126
|
-
|
|
127
|
-
session.sendSealed({ t: 'chunk', id, data: b64(chunk.subarray(offset, offset + 64 * 1024)) })
|
|
128
|
-
}
|
|
137
|
+
sendChunks(session, id, chunk, STREAM_CHUNK)
|
|
129
138
|
}
|
|
130
139
|
session.sendSealed({ t: 'end', id })
|
|
131
140
|
} catch (err) {
|
package/oc-remote.mjs
CHANGED
|
@@ -17,12 +17,7 @@ import { b64 } from './lib/crypto.mjs'
|
|
|
17
17
|
|
|
18
18
|
const require = createRequire(import.meta.url)
|
|
19
19
|
const NAME = 'oc-remote'
|
|
20
|
-
const VERSION = '2.
|
|
21
|
-
const LAUNCH_LABEL = 'com.raul.ocremote'
|
|
22
|
-
const LOG_PATH =
|
|
23
|
-
process.platform === 'darwin'
|
|
24
|
-
? path.join(os.homedir(), 'Library', 'Logs', 'oc-remote.log')
|
|
25
|
-
: path.join(os.homedir(), '.config', 'oc-remote', 'oc-remote.log')
|
|
20
|
+
const VERSION = '2.1.1'
|
|
26
21
|
const STARTED_AT = Date.now()
|
|
27
22
|
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
28
23
|
const LOOPBACK = '127.0.0.1'
|
|
@@ -51,13 +46,10 @@ augmentPath()
|
|
|
51
46
|
const HELP = `oc-remote v${VERSION} - remote companion for opencode
|
|
52
47
|
|
|
53
48
|
Usage:
|
|
54
|
-
oc-remote [options] Start the companion
|
|
49
|
+
oc-remote [options] Start the companion in the foreground
|
|
55
50
|
npx ocremote [--pair] Start and show the pairing QR (Node 22.13+)
|
|
56
|
-
npx ocremote --daemon Install as a background service (LaunchAgent)
|
|
57
51
|
oc-remote doctor Diagnose every link in the connection chain
|
|
58
|
-
oc-remote status Show config
|
|
59
|
-
oc-remote restart Restart the installed LaunchAgent
|
|
60
|
-
oc-remote logs Show the daemon log path and last lines
|
|
52
|
+
oc-remote status Show config and the live endpoint
|
|
61
53
|
oc-remote funnel on|off|status Enable/disable the stable Tailscale Funnel URL
|
|
62
54
|
oc-remote pair Print a fresh pairing QR for OpenCode Remote v2
|
|
63
55
|
oc-remote relay-status Relay identity, connection and paired devices
|
|
@@ -91,7 +83,7 @@ Options:
|
|
|
91
83
|
--help Show this help
|
|
92
84
|
`
|
|
93
85
|
|
|
94
|
-
const SUBCOMMANDS = new Set(['doctor', 'status', '
|
|
86
|
+
const SUBCOMMANDS = new Set(['doctor', 'status', 'version', 'funnel', 'pair', 'relay-status', 'unpair'])
|
|
95
87
|
|
|
96
88
|
const VALUE_FLAGS = new Set([
|
|
97
89
|
'dir',
|
|
@@ -107,7 +99,7 @@ const VALUE_FLAGS = new Set([
|
|
|
107
99
|
'relay',
|
|
108
100
|
'device-name',
|
|
109
101
|
])
|
|
110
|
-
const BOOL_FLAGS = new Set(['new-token', 'no-auth', 'mdns', 'no-mdns', 'print-qr', 'no-print-qr', 'help', 'pair'
|
|
102
|
+
const BOOL_FLAGS = new Set(['new-token', 'no-auth', 'mdns', 'no-mdns', 'print-qr', 'no-print-qr', 'help', 'pair'])
|
|
111
103
|
const TUNNEL_MODES = new Set(['auto', 'funnel', 'cloudflare', 'tailscale', 'none'])
|
|
112
104
|
const ALLOWED_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'])
|
|
113
105
|
|
|
@@ -1939,14 +1931,6 @@ function probeLocal(path, headers = {}, timeout = 3000) {
|
|
|
1939
1931
|
})
|
|
1940
1932
|
}
|
|
1941
1933
|
|
|
1942
|
-
async function launchAgentState() {
|
|
1943
|
-
if (process.platform !== 'darwin' || typeof process.getuid !== 'function') return 'not applicable (macOS only)'
|
|
1944
|
-
const result = await execCommand('launchctl', ['print', `gui/${process.getuid()}/${LAUNCH_LABEL}`], 5000)
|
|
1945
|
-
if (!result.ok) return 'not loaded'
|
|
1946
|
-
const match = /state = ([a-z]+)/.exec(result.stdout)
|
|
1947
|
-
return match ? match[1] : 'loaded'
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
1934
|
async function runSubcommand(name, argv) {
|
|
1951
1935
|
const parsed = parseArgs(argv)
|
|
1952
1936
|
const flags = parsed.error ? {} : parsed.flags
|
|
@@ -2071,35 +2055,8 @@ async function runSubcommand(name, argv) {
|
|
|
2071
2055
|
return 0
|
|
2072
2056
|
}
|
|
2073
2057
|
|
|
2074
|
-
if (name === 'logs') {
|
|
2075
|
-
out(LOG_PATH)
|
|
2076
|
-
if (fs.existsSync(LOG_PATH)) {
|
|
2077
|
-
const lines = fs.readFileSync(LOG_PATH, 'utf8').trimEnd().split('\n')
|
|
2078
|
-
out(lines.slice(-40).join('\n'))
|
|
2079
|
-
} else {
|
|
2080
|
-
out('(no log file yet)')
|
|
2081
|
-
}
|
|
2082
|
-
return 0
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
if (name === 'restart') {
|
|
2086
|
-
if (process.platform !== 'darwin') {
|
|
2087
|
-
out('the LaunchAgent only exists on macOS; run the companion in the foreground or use your service manager')
|
|
2088
|
-
return 1
|
|
2089
|
-
}
|
|
2090
|
-
const result = await execCommand('launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${LAUNCH_LABEL}`], 8000)
|
|
2091
|
-
if (!result.ok) {
|
|
2092
|
-
error(`could not restart ${LAUNCH_LABEL}: ${result.stderr.trim() || 'agent not loaded'}`)
|
|
2093
|
-
out('hint: bash companion/install.sh --dir <project> --tunnel <mode>')
|
|
2094
|
-
return 1
|
|
2095
|
-
}
|
|
2096
|
-
out(`restarted ${LAUNCH_LABEL}`)
|
|
2097
|
-
return 0
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
2058
|
if (name === 'status') {
|
|
2101
2059
|
const endpoint = readEndpointFile()
|
|
2102
|
-
const state = await launchAgentState()
|
|
2103
2060
|
const health = await probeLocal('/_ocremote/health')
|
|
2104
2061
|
const authenticated = token
|
|
2105
2062
|
? await probeLocal('/_ocremote/status', authHeader)
|
|
@@ -2107,7 +2064,6 @@ async function runSubcommand(name, argv) {
|
|
|
2107
2064
|
out(`oc-remote v${VERSION}`)
|
|
2108
2065
|
out(`config: ${configPaths().file}`)
|
|
2109
2066
|
out(`token: ${maskToken(token)}`)
|
|
2110
|
-
out(`launch agent: ${state}`)
|
|
2111
2067
|
out(`companion: ${health.ok ? `reachable on 127.0.0.1:${port}` : `NOT reachable on 127.0.0.1:${port}`}`)
|
|
2112
2068
|
out(`auth: ${authenticated.ok ? 'token accepted' : `token rejected (HTTP ${authenticated.status})`}`)
|
|
2113
2069
|
if (endpoint) {
|
|
@@ -2148,24 +2104,14 @@ async function runSubcommand(name, argv) {
|
|
|
2148
2104
|
check(
|
|
2149
2105
|
health.ok,
|
|
2150
2106
|
`companion reachable on 127.0.0.1:${port}`,
|
|
2151
|
-
'start it
|
|
2107
|
+
'start it in a terminal with `oc-remote --dir <project>`'
|
|
2152
2108
|
)
|
|
2153
2109
|
|
|
2154
2110
|
if (health.ok) {
|
|
2155
2111
|
const status = await probeLocal('/_ocremote/status', authHeader)
|
|
2156
2112
|
check(status.ok, `token accepted by the companion (HTTP ${status.status})`, 'rotate the token with `oc-remote --new-token`')
|
|
2157
2113
|
const upstream = await probeLocal('/global/health', authHeader, 5000)
|
|
2158
|
-
check(upstream.ok, `opencode upstream healthy (HTTP ${upstream.status})`, 'restart
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
const state = await launchAgentState()
|
|
2162
|
-
if (state === 'running' || state === 'loaded') {
|
|
2163
|
-
check(true, `LaunchAgent ${LAUNCH_LABEL}: ${state}`)
|
|
2164
|
-
} else {
|
|
2165
|
-
note(
|
|
2166
|
-
`LaunchAgent ${LAUNCH_LABEL}: ${state}`,
|
|
2167
|
-
'install it for always-on access: `bash companion/install.sh --dir <project> --tunnel auto`'
|
|
2168
|
-
)
|
|
2114
|
+
check(upstream.ok, `opencode upstream healthy (HTTP ${upstream.status})`, 'restart the foreground companion')
|
|
2169
2115
|
}
|
|
2170
2116
|
|
|
2171
2117
|
const lan = lanIPv4()
|
|
@@ -2206,7 +2152,7 @@ async function runSubcommand(name, argv) {
|
|
|
2206
2152
|
check(
|
|
2207
2153
|
endpointProbe.ok,
|
|
2208
2154
|
`endpoint reachable: ${endpoint.url} (HTTP ${endpointProbe.status})`,
|
|
2209
|
-
'check the tunnel
|
|
2155
|
+
'check the tunnel output or restart the foreground companion'
|
|
2210
2156
|
)
|
|
2211
2157
|
} else {
|
|
2212
2158
|
note('no endpoint published yet (LAN/tailscale only)', 'run the companion once to publish endpoint.json')
|
|
@@ -2263,7 +2209,7 @@ async function main() {
|
|
|
2263
2209
|
opts.ntfy = flags.ntfy !== undefined ? flags.ntfy : (cfg.ntfy ?? null)
|
|
2264
2210
|
opts.pair = Boolean(flags.pair)
|
|
2265
2211
|
opts.relay = flags.relay !== undefined ? String(flags.relay) :
|
|
2266
|
-
(cfg.relay ?? (
|
|
2212
|
+
(cfg.relay ?? (flags.pair && flags.tunnel === undefined ? 'wss://relay-production-9c9b.up.railway.app' : null))
|
|
2267
2213
|
if (opts.relay && flags.tunnel === undefined) opts.tunnel = 'none'
|
|
2268
2214
|
if (opts.relay && flags.host === undefined) opts.host = LOOPBACK
|
|
2269
2215
|
opts.deviceName = flags['device-name'] !== undefined ? String(flags['device-name']) : (cfg.deviceName ?? null)
|
|
@@ -2309,26 +2255,6 @@ async function main() {
|
|
|
2309
2255
|
if (!stat.isDirectory()) abort(`not a directory: ${dir}`)
|
|
2310
2256
|
opts.dir = dir
|
|
2311
2257
|
|
|
2312
|
-
if (flags.daemon) {
|
|
2313
|
-
if (process.platform !== 'darwin') {
|
|
2314
|
-
const say = (line) => process.stdout.write(`${line}\n`)
|
|
2315
|
-
say('--daemon installs a macOS LaunchAgent.')
|
|
2316
|
-
say('On Linux: create a systemd user unit running `npx ocremote --pair` (systemctl --user enable --now ocremote).')
|
|
2317
|
-
say('On Windows: add a Task Scheduler entry running `npx ocremote --pair` at logon.')
|
|
2318
|
-
say('Either way the foreground mode below works everywhere: npx ocremote --pair')
|
|
2319
|
-
return 1
|
|
2320
|
-
}
|
|
2321
|
-
const installer = path.join(HERE, 'install.sh')
|
|
2322
|
-
if (!fs.existsSync(installer)) abort(`install.sh not found next to oc-remote.mjs`)
|
|
2323
|
-
const args = [installer, '--dir', dir, '--port', String(opts.port)]
|
|
2324
|
-
if (opts.tunnel) args.push('--tunnel', opts.tunnel)
|
|
2325
|
-
if (opts.relay) args.push('--relay', opts.relay)
|
|
2326
|
-
if (opts.ntfy) args.push('--ntfy', String(opts.ntfy))
|
|
2327
|
-
if (opts.deviceName) args.push('--device-name', opts.deviceName)
|
|
2328
|
-
const child = spawn('bash', args, { stdio: 'inherit' })
|
|
2329
|
-
return await new Promise((resolve) => child.on('exit', (code) => resolve(code ?? 0)))
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
2258
|
if (flags.pair && cfg.token && !flags['new-token'] && flags.token === undefined && opts.port === Number(cfg.port)) {
|
|
2333
2259
|
const running = await probeLocal('/_ocremote/status', { authorization: `Bearer ${cfg.token}` })
|
|
2334
2260
|
if (running.ok) {
|
|
@@ -2356,7 +2282,7 @@ async function main() {
|
|
|
2356
2282
|
|
|
2357
2283
|
if (flags.pair) {
|
|
2358
2284
|
info('first run: starting opencode and preparing the pairing QR')
|
|
2359
|
-
info('keep this terminal open while you
|
|
2285
|
+
info('keep this terminal open while you use the app')
|
|
2360
2286
|
}
|
|
2361
2287
|
|
|
2362
2288
|
if (opts.ntfy) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocremote",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Control opencode on your computer from the OpenCode Remote iOS app: supervisor, pairing QR, relay and tunnel transports",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"oc-remote.mjs",
|
|
11
11
|
"lib/",
|
|
12
|
-
"vendor/"
|
|
13
|
-
"install.sh"
|
|
12
|
+
"vendor/"
|
|
14
13
|
],
|
|
15
14
|
"engines": {
|
|
16
15
|
"node": ">=22.13.0"
|
package/install.sh
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
LABEL="com.raul.ocremote"
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
ENTRY="$SCRIPT_DIR/oc-remote.mjs"
|
|
7
|
-
BIN_DIR="$HOME/.local/bin"
|
|
8
|
-
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
|
9
|
-
LOG_PATH="$HOME/Library/Logs/oc-remote.log"
|
|
10
|
-
PROJECT_DIR=""
|
|
11
|
-
PORT="4190"
|
|
12
|
-
TOKEN=""
|
|
13
|
-
TUNNEL="auto"
|
|
14
|
-
NTFY=""
|
|
15
|
-
RELAY=""
|
|
16
|
-
DEVICE_NAME=""
|
|
17
|
-
NO_LOAD="0"
|
|
18
|
-
UNINSTALL="0"
|
|
19
|
-
|
|
20
|
-
usage() {
|
|
21
|
-
cat <<USAGE
|
|
22
|
-
oc-remote installer
|
|
23
|
-
|
|
24
|
-
Usage:
|
|
25
|
-
companion/install.sh [--dir <path>] [--port <n>] [--token <str>] [--tunnel <mode>] [--ntfy <topic>]
|
|
26
|
-
[--relay <wss://url>] [--device-name <name>]
|
|
27
|
-
companion/install.sh --uninstall
|
|
28
|
-
|
|
29
|
-
Options:
|
|
30
|
-
--dir <path> Project directory opencode runs in (default: repo root)
|
|
31
|
-
--port <n> Companion port (default: 4190)
|
|
32
|
-
--token <str> Pairing token to persist (default: companion generates one)
|
|
33
|
-
--tunnel <mode> Remote access: auto | funnel | cloudflare | tailscale | none (default: auto)
|
|
34
|
-
funnel stable public HTTPS URL via Tailscale Funnel (one-time approval)
|
|
35
|
-
cloudflare public URL from anywhere with zero setup (downloads cloudflared)
|
|
36
|
-
--ntfy <topic> ntfy topic for push notifications + endpoint beacons
|
|
37
|
-
--relay <url> Relay v2 URL (wss://...) for anywhere access without tunnels
|
|
38
|
-
--device-name <name> Name shown on the phone when pairing (default: hostname)
|
|
39
|
-
--no-load Write the symlink and plist but do not load the LaunchAgent
|
|
40
|
-
--uninstall Unload the LaunchAgent, remove the plist and the symlink
|
|
41
|
-
-h, --help Show this help
|
|
42
|
-
|
|
43
|
-
Files:
|
|
44
|
-
symlink $BIN_DIR/oc-remote
|
|
45
|
-
plist $PLIST
|
|
46
|
-
log $LOG_PATH
|
|
47
|
-
config ~/.config/oc-remote/config.json (0600, written by oc-remote)
|
|
48
|
-
USAGE
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
while [[ $# -gt 0 ]]; do
|
|
52
|
-
case "$1" in
|
|
53
|
-
--dir)
|
|
54
|
-
[[ $# -ge 2 ]] || { echo "error: --dir needs a value" >&2; exit 2; }
|
|
55
|
-
PROJECT_DIR="$2"
|
|
56
|
-
shift 2
|
|
57
|
-
;;
|
|
58
|
-
--port)
|
|
59
|
-
[[ $# -ge 2 ]] || { echo "error: --port needs a value" >&2; exit 2; }
|
|
60
|
-
PORT="$2"
|
|
61
|
-
shift 2
|
|
62
|
-
;;
|
|
63
|
-
--token)
|
|
64
|
-
[[ $# -ge 2 ]] || { echo "error: --token needs a value" >&2; exit 2; }
|
|
65
|
-
TOKEN="$2"
|
|
66
|
-
shift 2
|
|
67
|
-
;;
|
|
68
|
-
--tunnel)
|
|
69
|
-
[[ $# -ge 2 ]] || { echo "error: --tunnel needs a value" >&2; exit 2; }
|
|
70
|
-
case "$2" in
|
|
71
|
-
auto|funnel|cloudflare|tailscale|none) TUNNEL="$2" ;;
|
|
72
|
-
*) echo "error: invalid --tunnel: $2 (expected auto | funnel | cloudflare | tailscale | none)" >&2; exit 2 ;;
|
|
73
|
-
esac
|
|
74
|
-
shift 2
|
|
75
|
-
;;
|
|
76
|
-
--ntfy)
|
|
77
|
-
[[ $# -ge 2 ]] || { echo "error: --ntfy needs a value" >&2; exit 2; }
|
|
78
|
-
NTFY="$2"
|
|
79
|
-
shift 2
|
|
80
|
-
;;
|
|
81
|
-
--relay)
|
|
82
|
-
[[ $# -ge 2 ]] || { echo "error: --relay needs a value" >&2; exit 2; }
|
|
83
|
-
RELAY="$2"
|
|
84
|
-
shift 2
|
|
85
|
-
;;
|
|
86
|
-
--device-name)
|
|
87
|
-
[[ $# -ge 2 ]] || { echo "error: --device-name needs a value" >&2; exit 2; }
|
|
88
|
-
DEVICE_NAME="$2"
|
|
89
|
-
shift 2
|
|
90
|
-
;;
|
|
91
|
-
--no-load)
|
|
92
|
-
NO_LOAD="1"
|
|
93
|
-
shift
|
|
94
|
-
;;
|
|
95
|
-
--uninstall)
|
|
96
|
-
UNINSTALL="1"
|
|
97
|
-
shift
|
|
98
|
-
;;
|
|
99
|
-
-h|--help)
|
|
100
|
-
usage
|
|
101
|
-
exit 0
|
|
102
|
-
;;
|
|
103
|
-
*)
|
|
104
|
-
echo "error: unknown argument: $1" >&2
|
|
105
|
-
usage >&2
|
|
106
|
-
exit 2
|
|
107
|
-
;;
|
|
108
|
-
esac
|
|
109
|
-
done
|
|
110
|
-
|
|
111
|
-
UID_NUM="$(id -u)"
|
|
112
|
-
DOMAIN="gui/$UID_NUM"
|
|
113
|
-
|
|
114
|
-
if [[ "$UNINSTALL" == "1" ]]; then
|
|
115
|
-
if launchctl bootout "$DOMAIN/$LABEL" >/dev/null 2>&1; then
|
|
116
|
-
echo "unloaded LaunchAgent $LABEL"
|
|
117
|
-
elif [[ -f "$PLIST" ]] && launchctl unload "$PLIST" >/dev/null 2>&1; then
|
|
118
|
-
echo "unloaded LaunchAgent $LABEL (legacy launchctl unload)"
|
|
119
|
-
else
|
|
120
|
-
echo "LaunchAgent $LABEL was not loaded"
|
|
121
|
-
fi
|
|
122
|
-
rm -f "$PLIST"
|
|
123
|
-
rm -f "$BIN_DIR/oc-remote"
|
|
124
|
-
echo "removed $PLIST"
|
|
125
|
-
echo "removed $BIN_DIR/oc-remote"
|
|
126
|
-
echo "kept ~/.config/oc-remote/config.json (delete it manually to forget the token)"
|
|
127
|
-
exit 0
|
|
128
|
-
fi
|
|
129
|
-
|
|
130
|
-
if [[ -z "$PROJECT_DIR" ]]; then
|
|
131
|
-
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
132
|
-
fi
|
|
133
|
-
if [[ ! -d "$PROJECT_DIR" ]]; then
|
|
134
|
-
echo "error: directory not found: $PROJECT_DIR" >&2
|
|
135
|
-
exit 1
|
|
136
|
-
fi
|
|
137
|
-
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
|
138
|
-
|
|
139
|
-
if [[ ! -f "$ENTRY" ]]; then
|
|
140
|
-
echo "error: oc-remote.mjs not found next to install.sh" >&2
|
|
141
|
-
exit 1
|
|
142
|
-
fi
|
|
143
|
-
|
|
144
|
-
if [[ ! "$PORT" =~ ^[0-9]+$ ]] || (( PORT < 1 || PORT > 65535 )); then
|
|
145
|
-
echo "error: invalid port: $PORT" >&2
|
|
146
|
-
exit 1
|
|
147
|
-
fi
|
|
148
|
-
|
|
149
|
-
NODE_BIN="$(command -v node || true)"
|
|
150
|
-
if [[ -z "$NODE_BIN" ]]; then
|
|
151
|
-
echo "error: node not found in PATH" >&2
|
|
152
|
-
exit 1
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
xml_escape() {
|
|
156
|
-
local value="$1"
|
|
157
|
-
value="${value//&/&}"
|
|
158
|
-
value="${value//</<}"
|
|
159
|
-
value="${value//>/>}"
|
|
160
|
-
printf '%s' "$value"
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
mkdir -p "$BIN_DIR" "$HOME/Library/LaunchAgents" "$HOME/Library/Logs"
|
|
164
|
-
chmod +x "$ENTRY"
|
|
165
|
-
ln -sfn "$ENTRY" "$BIN_DIR/oc-remote"
|
|
166
|
-
|
|
167
|
-
{
|
|
168
|
-
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
|
169
|
-
echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
|
|
170
|
-
echo '<plist version="1.0">'
|
|
171
|
-
echo '<dict>'
|
|
172
|
-
echo ' <key>Label</key>'
|
|
173
|
-
echo " <string>$LABEL</string>"
|
|
174
|
-
echo ' <key>ProgramArguments</key>'
|
|
175
|
-
echo ' <array>'
|
|
176
|
-
echo " <string>$(xml_escape "$NODE_BIN")</string>"
|
|
177
|
-
echo " <string>$(xml_escape "$BIN_DIR/oc-remote")</string>"
|
|
178
|
-
echo ' <string>--no-print-qr</string>'
|
|
179
|
-
echo ' <string>--dir</string>'
|
|
180
|
-
echo " <string>$(xml_escape "$PROJECT_DIR")</string>"
|
|
181
|
-
echo ' <string>--port</string>'
|
|
182
|
-
echo " <string>$PORT</string>"
|
|
183
|
-
echo ' <string>--tunnel</string>'
|
|
184
|
-
echo " <string>$TUNNEL</string>"
|
|
185
|
-
if [[ -n "$NTFY" ]]; then
|
|
186
|
-
echo ' <string>--ntfy</string>'
|
|
187
|
-
echo " <string>$(xml_escape "$NTFY")</string>"
|
|
188
|
-
fi
|
|
189
|
-
if [[ -n "$RELAY" ]]; then
|
|
190
|
-
echo ' <string>--relay</string>'
|
|
191
|
-
echo " <string>$(xml_escape "$RELAY")</string>"
|
|
192
|
-
fi
|
|
193
|
-
if [[ -n "$DEVICE_NAME" ]]; then
|
|
194
|
-
echo ' <string>--device-name</string>'
|
|
195
|
-
echo " <string>$(xml_escape "$DEVICE_NAME")</string>"
|
|
196
|
-
fi
|
|
197
|
-
if [[ -n "$TOKEN" ]]; then
|
|
198
|
-
echo ' <string>--token</string>'
|
|
199
|
-
echo " <string>$(xml_escape "$TOKEN")</string>"
|
|
200
|
-
fi
|
|
201
|
-
echo ' </array>'
|
|
202
|
-
echo ' <key>WorkingDirectory</key>'
|
|
203
|
-
echo " <string>$(xml_escape "$PROJECT_DIR")</string>"
|
|
204
|
-
echo ' <key>EnvironmentVariables</key>'
|
|
205
|
-
echo ' <dict>'
|
|
206
|
-
echo ' <key>PATH</key>'
|
|
207
|
-
echo " <string>$(xml_escape "$(dirname "$NODE_BIN"):/opt/homebrew/bin:/usr/local/bin:$BIN_DIR:$HOME/.opencode/bin:/usr/bin:/bin:/usr/sbin:/sbin")</string>"
|
|
208
|
-
echo ' </dict>'
|
|
209
|
-
echo ' <key>RunAtLoad</key>'
|
|
210
|
-
echo ' <true/>'
|
|
211
|
-
echo ' <key>KeepAlive</key>'
|
|
212
|
-
echo ' <true/>'
|
|
213
|
-
echo ' <key>ThrottleInterval</key>'
|
|
214
|
-
echo ' <integer>10</integer>'
|
|
215
|
-
echo ' <key>StandardOutPath</key>'
|
|
216
|
-
echo " <string>$(xml_escape "$LOG_PATH")</string>"
|
|
217
|
-
echo ' <key>StandardErrorPath</key>'
|
|
218
|
-
echo " <string>$(xml_escape "$LOG_PATH")</string>"
|
|
219
|
-
echo '</dict>'
|
|
220
|
-
echo '</plist>'
|
|
221
|
-
} > "$PLIST"
|
|
222
|
-
chmod 600 "$PLIST"
|
|
223
|
-
|
|
224
|
-
plutil -lint "$PLIST"
|
|
225
|
-
|
|
226
|
-
if [[ "$NO_LOAD" == "1" ]]; then
|
|
227
|
-
echo "wrote $PLIST (not loaded)"
|
|
228
|
-
echo "wrote $BIN_DIR/oc-remote -> $ENTRY"
|
|
229
|
-
exit 0
|
|
230
|
-
fi
|
|
231
|
-
|
|
232
|
-
launchctl bootout "$DOMAIN/$LABEL" >/dev/null 2>&1 || true
|
|
233
|
-
if ! launchctl bootstrap "$DOMAIN" "$PLIST" >/dev/null 2>&1; then
|
|
234
|
-
launchctl load -w "$PLIST"
|
|
235
|
-
fi
|
|
236
|
-
launchctl kickstart -k "$DOMAIN/$LABEL" >/dev/null 2>&1 || true
|
|
237
|
-
|
|
238
|
-
echo "installed LaunchAgent $LABEL"
|
|
239
|
-
echo " binary: $BIN_DIR/oc-remote -> $ENTRY"
|
|
240
|
-
echo " plist: $PLIST"
|
|
241
|
-
echo " log: $LOG_PATH"
|
|
242
|
-
echo " port: $PORT"
|
|
243
|
-
echo " tunnel: $TUNNEL"
|
|
244
|
-
[[ -n "$RELAY" ]] && echo " relay: $RELAY"
|
|
245
|
-
echo
|
|
246
|
-
ENDPOINT_JSON="$HOME/.config/oc-remote/endpoint.json"
|
|
247
|
-
for _ in $(seq 1 15); do
|
|
248
|
-
[[ -f "$ENDPOINT_JSON" ]] && break
|
|
249
|
-
sleep 1
|
|
250
|
-
done
|
|
251
|
-
if [[ -f "$ENDPOINT_JSON" ]]; then
|
|
252
|
-
ENDPOINT_URL="$(node -e "try{const e=require('$ENDPOINT_JSON');process.stdout.write(e.url||'')}catch{}" 2>/dev/null || true)"
|
|
253
|
-
if [[ -n "$ENDPOINT_URL" ]]; then
|
|
254
|
-
echo "endpoint: $ENDPOINT_URL"
|
|
255
|
-
fi
|
|
256
|
-
fi
|
|
257
|
-
echo "Pairing details (URL, token, QR) are in the log:"
|
|
258
|
-
echo " tail -f \"$LOG_PATH\""
|
|
259
|
-
echo
|
|
260
|
-
echo "Verify the whole chain with: oc-remote doctor"
|
|
261
|
-
echo "Restart the daemon with: oc-remote restart"
|
|
262
|
-
echo "Uninstall with: $0 --uninstall"
|