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.
- package/README.md +11 -1
- package/oc-remote.mjs +67 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
# oc-remote
|
|
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.
|
|
20
|
+
const VERSION = '1.3.3'
|
|
21
21
|
const LAUNCH_LABEL = 'com.raul.ocremote'
|
|
22
|
-
const LOG_PATH =
|
|
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
|
|
302
|
-
if (
|
|
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
|
-
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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 =
|
|
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
|
}
|