ocremote 1.3.2 → 1.3.3

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.
Files changed (3) hide show
  1. package/README.md +11 -1
  2. package/oc-remote.mjs +67 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,14 @@
1
- # oc-remote companion
1
+ # oc-remote
2
+
3
+ > **Plataformas.** El companion es Node puro (>= 20) y funciona en 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
9
+ > `cloudflared` para la plataforma correcta. Windows está corregido en código
10
+ > (rutas, `.exe`, `;` en PATH, cloudflared .exe) pero no verificado en una máquina
11
+ > Windows real. companion
2
12
 
3
13
  Proceso Node sin dependencias externas (solo APIs nativas de Node 22+) que
4
14
  convierte un Mac en un host de **opencode** accesible desde la app iOS
package/oc-remote.mjs CHANGED
@@ -17,9 +17,12 @@ 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 = '1.3.2'
20
+ const VERSION = '1.3.3'
21
21
  const LAUNCH_LABEL = 'com.raul.ocremote'
22
- const LOG_PATH = path.join(os.homedir(), 'Library', 'Logs', 'oc-remote.log')
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')
23
26
  const STARTED_AT = Date.now()
24
27
  const HERE = path.dirname(fileURLToPath(import.meta.url))
25
28
  const LOOPBACK = '127.0.0.1'
@@ -82,6 +85,7 @@ Options:
82
85
  --no-auth Disable token auth (DANGEROUS, LAN becomes open)
83
86
  --opencode <path> opencode binary (default: ~/.opencode/bin/opencode)
84
87
  --no-mdns Do not publish _ocremote._tcp via Bonjour
88
+ (macOS only; enabled there by default)
85
89
  --print-qr Print pairing QR at startup (default)
86
90
  --no-print-qr Do not print the pairing QR
87
91
  --help Show this help
@@ -103,7 +107,7 @@ const VALUE_FLAGS = new Set([
103
107
  'relay',
104
108
  'device-name',
105
109
  ])
106
- const BOOL_FLAGS = new Set(['new-token', 'no-auth', 'no-mdns', 'print-qr', 'no-print-qr', 'help', 'pair', 'daemon'])
110
+ const BOOL_FLAGS = new Set(['new-token', 'no-auth', 'mdns', 'no-mdns', 'print-qr', 'no-print-qr', 'help', 'pair', 'daemon'])
107
111
  const TUNNEL_MODES = new Set(['auto', 'funnel', 'cloudflare', 'tailscale', 'none'])
108
112
  const ALLOWED_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'])
109
113
 
@@ -129,6 +133,7 @@ let funnelMonitorTimer = null
129
133
  let transportTimer = null
130
134
  let cachedTailscaleIp = null
131
135
  let endpointTimer = null
136
+ let inviteRefreshTimer = null
132
137
  let lastBeaconKey = ''
133
138
  let watchdogTimer = null
134
139
  let watchdogFailures = 0
@@ -298,22 +303,31 @@ function saveConfig(cfg) {
298
303
  function resolveOpencodeBin(flagValue) {
299
304
  if (flagValue) return flagValue
300
305
  if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN
301
- const homeBin = path.join(os.homedir(), '.opencode', 'bin', 'opencode')
302
- if (fs.existsSync(homeBin)) return homeBin
306
+ const candidates = [path.join(os.homedir(), '.opencode', 'bin', 'opencode')]
307
+ if (process.platform === 'win32') {
308
+ candidates.unshift(path.join(os.homedir(), '.opencode', 'bin', 'opencode.exe'))
309
+ }
310
+ for (const candidate of candidates) {
311
+ if (fs.existsSync(candidate)) return candidate
312
+ }
303
313
  return 'opencode'
304
314
  }
305
315
 
306
316
  function locateBin(bin) {
307
- if (bin.includes('/')) {
317
+ if (bin.includes('/') || bin.includes('\\')) {
308
318
  return fs.existsSync(bin) ? bin : null
309
319
  }
310
- for (const dir of (process.env.PATH || '').split(':')) {
320
+ const suffixes = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : ['']
321
+ const separator = process.platform === 'win32' ? ';' : ':'
322
+ for (const dir of (process.env.PATH || '').split(separator)) {
311
323
  if (!dir) continue
312
- const candidate = path.join(dir, bin)
313
- try {
314
- fs.accessSync(candidate, fs.constants.X_OK)
315
- return candidate
316
- } catch {}
324
+ for (const suffix of suffixes) {
325
+ const candidate = path.join(dir, bin + suffix)
326
+ try {
327
+ fs.accessSync(candidate, fs.constants.X_OK)
328
+ return candidate
329
+ } catch {}
330
+ }
317
331
  }
318
332
  return null
319
333
  }
@@ -1205,10 +1219,15 @@ async function ensureCloudflared() {
1205
1219
  const existing = findCloudflared()
1206
1220
  if (existing) return existing
1207
1221
  const arch = process.arch === 'arm64' ? 'arm64' : 'amd64'
1208
- if (process.platform !== 'darwin' && process.platform !== 'linux') {
1222
+ if (process.platform !== 'darwin' && process.platform !== 'linux' && process.platform !== 'win32') {
1209
1223
  throw new Error(`automatic cloudflared install is not supported on ${process.platform}; install it manually`)
1210
1224
  }
1211
- const asset = process.platform === 'darwin' ? `cloudflared-darwin-${arch}.tgz` : `cloudflared-linux-${arch}`
1225
+ const asset =
1226
+ process.platform === 'darwin'
1227
+ ? `cloudflared-darwin-${arch}.tgz`
1228
+ : process.platform === 'win32'
1229
+ ? `cloudflared-windows-${arch}.exe`
1230
+ : `cloudflared-linux-${arch}`
1212
1231
  const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`
1213
1232
  info(`downloading cloudflared (${asset}) ...`)
1214
1233
  fs.mkdirSync(configBinDir(), { recursive: true, mode: 0o700 })
@@ -1535,7 +1554,7 @@ function noteWatchdogFailure() {
1535
1554
  } catch {}
1536
1555
  }
1537
1556
 
1538
- function printPairing() {
1557
+ function printPairing(options = {}) {
1539
1558
  for (const entry of allUrls) info(`url: ${entry.url} [${entry.label}]`)
1540
1559
  info(`pairing token: ${remoteToken}`)
1541
1560
  const publicEntry = allUrls.find((entry) => entry.label === 'public' || entry.label === 'tunnel')
@@ -1549,8 +1568,12 @@ function printPairing() {
1549
1568
  } else {
1550
1569
  info('remote access: LAN only - run `oc-remote --tunnel funnel` (stable URL) or --tunnel cloudflare')
1551
1570
  }
1571
+ if (opts.relay) {
1572
+ info('pairing: relay mode, the invite QR below is the one to scan')
1573
+ return
1574
+ }
1552
1575
  info(`pairing deep link: ${pairLink()}`)
1553
- if (!opts.printQr) return
1576
+ if (options.qr === false || !opts.printQr) return
1554
1577
  if (!qrcode) {
1555
1578
  warn('QR unavailable: companion/vendor/qrcode.cjs not found')
1556
1579
  return
@@ -1603,7 +1626,24 @@ async function startRelay() {
1603
1626
  const payload = await createInvitePayload()
1604
1627
  info(`relay: ${opts.relay}`)
1605
1628
  info(`pairing v2 link: ${payload.url}`)
1629
+ if (payload.expiresAt) {
1630
+ info(`invite expires at ${new Date(payload.expiresAt).toLocaleTimeString()} (15 minutes, single use)`)
1631
+ }
1606
1632
  printInviteQr(payload.url)
1633
+ if (opts.pair) {
1634
+ clearInterval(inviteRefreshTimer)
1635
+ inviteRefreshTimer = setInterval(async () => {
1636
+ if (shuttingDown) return
1637
+ try {
1638
+ const fresh = await createInvitePayload()
1639
+ info(`invite refreshed (previous one expired or about to): ${fresh.url}`)
1640
+ printInviteQr(fresh.url)
1641
+ } catch (err) {
1642
+ warn(`could not refresh the invite: ${err.message}`)
1643
+ }
1644
+ }, 10 * 60_000)
1645
+ inviteRefreshTimer.unref?.()
1646
+ }
1607
1647
  }
1608
1648
 
1609
1649
  function printBanner(health) {
@@ -1616,14 +1656,14 @@ function printBanner(health) {
1616
1656
  if (opts.noAuth) info('pair page: disabled (--no-auth)')
1617
1657
  if (opts.relay) info(`relay: ${opts.relay} (device ${opts.deviceName || os.hostname()})`)
1618
1658
  info('self-check: oc-remote doctor')
1619
- printPairing()
1659
+ printPairing({ qr: false })
1620
1660
  }
1621
1661
 
1622
1662
  function startMdns() {
1623
1663
  if (!opts.mdns) return
1624
1664
  const dnsSd = '/usr/bin/dns-sd'
1625
1665
  if (!fs.existsSync(dnsSd)) {
1626
- warn('mDNS: /usr/bin/dns-sd not found; skipping Bonjour registration')
1666
+ if (opts.mdnsExplicit) warn('mDNS: /usr/bin/dns-sd not found; skipping Bonjour registration')
1627
1667
  return
1628
1668
  }
1629
1669
  const hostLabel = os.hostname().replace(/\.local\.?$/i, '')
@@ -1739,6 +1779,7 @@ function shutdown(signal) {
1739
1779
  info(`received ${signal}; shutting down`)
1740
1780
  clearTimeout(restartTimer)
1741
1781
  clearInterval(endpointTimer)
1782
+ clearInterval(inviteRefreshTimer)
1742
1783
  clearInterval(watchdogTimer)
1743
1784
  clearInterval(funnelMonitorTimer)
1744
1785
  clearInterval(transportTimer)
@@ -1882,6 +1923,7 @@ function probeLocal(path, headers = {}, timeout = 3000) {
1882
1923
  }
1883
1924
 
1884
1925
  async function launchAgentState() {
1926
+ if (process.platform !== 'darwin' || typeof process.getuid !== 'function') return 'not applicable (macOS only)'
1885
1927
  const result = await execCommand('launchctl', ['print', `gui/${process.getuid()}/${LAUNCH_LABEL}`], 5000)
1886
1928
  if (!result.ok) return 'not loaded'
1887
1929
  const match = /state = ([a-z]+)/.exec(result.stdout)
@@ -2028,6 +2070,10 @@ async function runSubcommand(name, argv) {
2028
2070
  }
2029
2071
 
2030
2072
  if (name === 'restart') {
2073
+ if (process.platform !== 'darwin') {
2074
+ out('the LaunchAgent only exists on macOS; run the companion in the foreground or use your service manager')
2075
+ return 1
2076
+ }
2031
2077
  const result = await execCommand('launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${LAUNCH_LABEL}`], 8000)
2032
2078
  if (!result.ok) {
2033
2079
  error(`could not restart ${LAUNCH_LABEL}: ${result.stderr.trim() || 'agent not loaded'}`)
@@ -2206,7 +2252,8 @@ async function main() {
2206
2252
  ? Number(flags['utility-port'])
2207
2253
  : Number(cfg.utilityPort ?? 0) || null
2208
2254
  opts.noAuth = Boolean(flags['no-auth'])
2209
- opts.mdns = !flags['no-mdns']
2255
+ opts.mdns = flags.mdns === true || (!flags['no-mdns'] && process.platform === 'darwin')
2256
+ opts.mdnsExplicit = flags.mdns === true
2210
2257
  opts.printQr = flags['print-qr'] !== false
2211
2258
 
2212
2259
  if (!TUNNEL_MODES.has(opts.tunnel)) {
@@ -2378,6 +2425,7 @@ async function main() {
2378
2425
  startTransportWatcher()
2379
2426
  startWatchdog()
2380
2427
  await startTunnel()
2428
+ if (!opts.relay) printPairing()
2381
2429
  await startRelay()
2382
2430
  return 0
2383
2431
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocremote",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
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": {