ocremote 2.1.0 → 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 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. Solo en macOS:
5
- > `--daemon` (LaunchAgent) y mDNS (`--no-mdns` lo desactiva; en otros sistemas ni
6
- > se intenta). En Linux, para tenerlo siempre activo usa una unidad de usuario de
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.0.0, preparado para publicar)
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`. En macOS, `npx ocremote --daemon` instala el servicio.
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. La versión publicada no cambia hasta publicar el
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, LaunchAgent, LAN, Tailscale/Funnel, endpoint público y ntfy. Salida `ok`/`FAIL` con el arreglo de cada fallo |
104
- | `oc-remote status` | Config, estado del LaunchAgent, endpoint actual (de `endpoint.json`) y prueba de auth |
105
- | `oc-remote restart` | `launchctl kickstart -k` del LaunchAgent |
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/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.1.0'
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 (and the daemon entry point)
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, daemon state and live endpoint
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', 'restart', 'logs', 'version', 'funnel', 'pair', 'relay-status', 'unpair'])
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', 'daemon'])
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 (`oc-remote --dir <project>`) or `bash companion/install.sh --dir <project>`'
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 with `oc-remote 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: `oc-remote logs` or restart with `oc-remote restart`'
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 ?? ((flags.pair || flags.daemon) && flags.tunnel === undefined ? 'wss://relay-production-9c9b.up.railway.app' : null))
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 pair; for an always-on service use --daemon')
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.1.0",
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//&/&amp;}"
158
- value="${value//</&lt;}"
159
- value="${value//>/&gt;}"
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"